mirror of
https://github.com/MrLowPr/overdrive.git
synced 2026-10-18 23:01:07 +02:00
149 lines
6.1 KiB
C++
149 lines
6.1 KiB
C++
// Copyright (C) 2026 half144 and PS5 React contributors
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Additional attribution term: see LICENSE-ATTRIBUTION.
|
|
#include "video_subtitles.hpp"
|
|
|
|
extern "C" {
|
|
#include <libavcodec/avcodec.h>
|
|
#include <libavformat/avformat.h>
|
|
}
|
|
|
|
#include <algorithm>
|
|
|
|
namespace video {
|
|
|
|
namespace {
|
|
constexpr AVRational kMicroseconds = {1, 1000000};
|
|
// A cue without an end shows this long, or until the next one starts.
|
|
constexpr std::int64_t kOpenEndedUs = 5'000'000;
|
|
|
|
// The Text field of an ASS Dialogue event ("ReadOrder,Layer,Style,Name,MarginL,MarginR,MarginV,Effect,Text"),
|
|
// without override blocks, with its line breaks as newlines.
|
|
std::string ass_text(const char* line) {
|
|
const char* text = line;
|
|
for (int commas = 0; *text && commas < 8; ++text)
|
|
if (*text == ',') ++commas;
|
|
std::string out;
|
|
for (const char* p = text; *p; ++p) {
|
|
if (*p == '{') {
|
|
while (*p && *p != '}') ++p;
|
|
if (!*p) break;
|
|
} else if (*p == '\\' && (p[1] == 'N' || p[1] == 'n')) {
|
|
out += '\n';
|
|
++p;
|
|
} else if (*p == '\\' && p[1] == 'h') {
|
|
out += ' ';
|
|
++p;
|
|
} else {
|
|
out += *p;
|
|
}
|
|
}
|
|
// Trailing line breaks and spaces carry nothing.
|
|
while (!out.empty() && (out.back() == '\n' || out.back() == ' ' || out.back() == '\r')) out.pop_back();
|
|
return out;
|
|
}
|
|
|
|
void insert_sorted(std::vector<Cue>& cues, Cue cue) {
|
|
const auto at = std::upper_bound(cues.begin(), cues.end(), cue.start_us,
|
|
[](std::int64_t t, const Cue& c) { return t < c.start_us; });
|
|
cues.insert(at, std::move(cue));
|
|
}
|
|
} // namespace
|
|
|
|
bool is_text_subtitle(const AVCodecParameters* p) {
|
|
if (!p || p->codec_type != AVMEDIA_TYPE_SUBTITLE) return false;
|
|
const AVCodecDescriptor* descriptor = avcodec_descriptor_get(p->codec_id);
|
|
return descriptor && (descriptor->props & AV_CODEC_PROP_TEXT_SUB) && avcodec_find_decoder(p->codec_id);
|
|
}
|
|
|
|
SubtitleDecoder::SubtitleDecoder(const AVCodecParameters* parameters, int time_base_num, int time_base_den)
|
|
: time_base_num_(time_base_num), time_base_den_(time_base_den) {
|
|
const AVCodec* codec = avcodec_find_decoder(parameters->codec_id);
|
|
if (!codec || !(context_ = avcodec_alloc_context3(codec))) return;
|
|
avcodec_parameters_to_context(context_, parameters);
|
|
context_->pkt_timebase = {time_base_num, time_base_den};
|
|
if (avcodec_open2(context_, codec, nullptr) < 0) avcodec_free_context(&context_);
|
|
}
|
|
|
|
SubtitleDecoder::~SubtitleDecoder() { avcodec_free_context(&context_); }
|
|
|
|
void SubtitleDecoder::decode(AVPacket* packet, std::int64_t start_us, std::vector<Cue>& out) {
|
|
if (!context_ || !packet) return;
|
|
AVSubtitle subtitle{};
|
|
int got = 0;
|
|
if (avcodec_decode_subtitle2(context_, &subtitle, &got, packet) < 0 || !got) return;
|
|
const AVRational base = {time_base_num_, time_base_den_};
|
|
const std::int64_t pts = packet->pts != AV_NOPTS_VALUE ? av_rescale_q(packet->pts, base, kMicroseconds) - start_us
|
|
: subtitle.pts != AV_NOPTS_VALUE ? subtitle.pts - start_us : 0;
|
|
Cue cue;
|
|
cue.start_us = pts + subtitle.start_display_time * 1000;
|
|
if (subtitle.end_display_time && subtitle.end_display_time != UINT32_MAX)
|
|
cue.end_us = pts + static_cast<std::int64_t>(subtitle.end_display_time) * 1000;
|
|
else if (packet->duration > 0)
|
|
cue.end_us = pts + av_rescale_q(packet->duration, base, kMicroseconds);
|
|
else
|
|
cue.end_us = cue.start_us + kOpenEndedUs;
|
|
for (unsigned i = 0; i < subtitle.num_rects; ++i) {
|
|
const AVSubtitleRect* rect = subtitle.rects[i];
|
|
const std::string text = rect->type == SUBTITLE_ASS && rect->ass ? ass_text(rect->ass)
|
|
: rect->type == SUBTITLE_TEXT && rect->text ? rect->text : "";
|
|
if (text.empty()) continue;
|
|
if (!cue.text.empty()) cue.text += '\n';
|
|
cue.text += text;
|
|
}
|
|
avsubtitle_free(&subtitle);
|
|
if (!cue.text.empty() && cue.end_us > cue.start_us) insert_sorted(out, std::move(cue));
|
|
}
|
|
|
|
bool load_subtitle_file(const std::string& url, const std::vector<std::string>& headers, std::vector<Cue>& cues,
|
|
std::string& error) {
|
|
AVFormatContext* format = nullptr;
|
|
AVDictionary* settings = nullptr;
|
|
std::string joined;
|
|
for (const std::string& header : headers) joined += header + "\r\n";
|
|
if (!joined.empty()) av_dict_set(&settings, "headers", joined.c_str(), 0);
|
|
av_dict_set(&settings, "rw_timeout", "15000000", 0);
|
|
int result = avformat_open_input(&format, url.c_str(), nullptr, &settings);
|
|
av_dict_free(&settings);
|
|
if (result >= 0) result = avformat_find_stream_info(format, nullptr);
|
|
int index = -1;
|
|
for (unsigned i = 0; result >= 0 && i < format->nb_streams; ++i)
|
|
if (is_text_subtitle(format->streams[i]->codecpar)) {
|
|
index = static_cast<int>(i);
|
|
break;
|
|
}
|
|
if (result < 0 || index < 0) {
|
|
char text[AV_ERROR_MAX_STRING_SIZE] = {};
|
|
if (result < 0) av_strerror(result, text, sizeof text);
|
|
error = "video.addSubtitle " + url + ": " + (result < 0 ? text : "no text subtitles in it");
|
|
avformat_close_input(&format);
|
|
return false;
|
|
}
|
|
const AVStream* stream = format->streams[index];
|
|
SubtitleDecoder decoder(stream->codecpar, stream->time_base.num, stream->time_base.den);
|
|
AVPacket* packet = av_packet_alloc();
|
|
const std::int64_t start_us = format->start_time != AV_NOPTS_VALUE ? format->start_time : 0;
|
|
while (decoder.ok() && av_read_frame(format, packet) >= 0) {
|
|
if (packet->stream_index == index) decoder.decode(packet, start_us, cues);
|
|
av_packet_unref(packet);
|
|
}
|
|
av_packet_free(&packet);
|
|
avformat_close_input(&format);
|
|
if (!decoder.ok()) error = "video.addSubtitle " + url + ": no decoder for its subtitles";
|
|
return decoder.ok();
|
|
}
|
|
|
|
std::string cue_text(const std::vector<Cue>& cues, std::int64_t at_us) {
|
|
std::string out;
|
|
// Cues starting after `at_us` cannot show; earlier ones are checked back to the longest a cue lasts.
|
|
auto end = std::upper_bound(cues.begin(), cues.end(), at_us, [](std::int64_t t, const Cue& c) { return t < c.start_us; });
|
|
for (auto it = cues.begin(); it != end; ++it) {
|
|
if (it->end_us <= at_us) continue;
|
|
if (!out.empty()) out += '\n';
|
|
out += it->text;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
} // namespace video
|