Files
QtPotato/src/subtitles/subtitleextractor.cpp
T

179 lines
6.5 KiB
C++

#include "subtitleextractor.h"
#include "trackselection.h"
#include <KLocalizedString>
#include <memory>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/error.h>
}
namespace
{
struct FormatContextDeleter {
void operator()(AVFormatContext *context) const
{
avformat_close_input(&context);
}
};
struct CodecContextDeleter {
void operator()(AVCodecContext *context) const
{
avcodec_free_context(&context);
}
};
struct PacketDeleter {
void operator()(AVPacket *packet) const
{
av_packet_free(&packet);
}
};
QString ffmpegError(int code)
{
char buffer[AV_ERROR_MAX_STRING_SIZE] = {};
av_strerror(code, buffer, sizeof(buffer));
return QString::fromUtf8(buffer);
}
qint64 toMs(int64_t value, AVRational timeBase)
{
return av_rescale_q(value, timeBase, AVRational{1, 1000});
}
// ASS events from the decoder look like "ReadOrder,Layer,Style,Name,MarginL,MarginR,MarginV,Effect,Text".
QString assRectText(const char *ass)
{
const QString event = QString::fromUtf8(ass);
qsizetype from = 0;
for (int field = 0; field < 8; ++field) {
from = event.indexOf(u',', from);
if (from < 0) {
return {};
}
++from;
}
return Subtitles::assEventText(QStringView(event).mid(from));
}
}
namespace Subtitles
{
ExtractResult extractEmbedded(const QString &path, int streamIndex, const std::atomic_bool &cancelled)
{
AVFormatContext *rawFormat = nullptr;
if (const int ret = avformat_open_input(&rawFormat, path.toUtf8().constData(), nullptr, nullptr); ret < 0) {
return {{}, i18n("Could not open %1: %2", path, ffmpegError(ret))};
}
std::unique_ptr<AVFormatContext, FormatContextDeleter> format(rawFormat);
if (streamIndex < 0 || streamIndex >= static_cast<int>(format->nb_streams)) {
return {{}, i18n("Subtitle stream %1 does not exist in this file.", streamIndex)};
}
AVStream *stream = format->streams[streamIndex];
const AVCodecDescriptor *descriptor = avcodec_descriptor_get(stream->codecpar->codec_id);
if (stream->codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE || !descriptor) {
return {{}, i18n("Stream %1 is not a subtitle stream.", streamIndex)};
}
if (descriptor->props & AV_CODEC_PROP_BITMAP_SUB) {
return {{}, i18n("This subtitle track is made of images (%1) and can't be translated.", QString::fromUtf8(descriptor->name))};
}
const AVCodec *codec = avcodec_find_decoder(stream->codecpar->codec_id);
if (!codec) {
return {{}, i18n("No decoder for subtitle format %1.", QString::fromUtf8(descriptor->name))};
}
std::unique_ptr<AVCodecContext, CodecContextDeleter> decoder(avcodec_alloc_context3(codec));
avcodec_parameters_to_context(decoder.get(), stream->codecpar);
decoder->pkt_timebase = stream->time_base;
if (const int ret = avcodec_open2(decoder.get(), codec, nullptr); ret < 0) {
return {{}, i18n("Could not open the subtitle decoder: %1", ffmpegError(ret))};
}
// Only the subtitle stream matters; let the demuxer skip the rest.
for (unsigned i = 0; i < format->nb_streams; ++i) {
if (static_cast<int>(i) != streamIndex) {
format->streams[i]->discard = AVDISCARD_ALL;
}
}
ExtractResult result;
std::unique_ptr<AVPacket, PacketDeleter> packet(av_packet_alloc());
while (av_read_frame(format.get(), packet.get()) >= 0) {
if (cancelled) {
return {};
}
if (packet->stream_index != streamIndex || packet->pts == AV_NOPTS_VALUE) {
av_packet_unref(packet.get());
continue;
}
AVSubtitle subtitle{};
int gotSubtitle = 0;
if (avcodec_decode_subtitle2(decoder.get(), &subtitle, &gotSubtitle, packet.get()) >= 0 && gotSubtitle) {
const qint64 start = toMs(packet->pts, stream->time_base);
qint64 end = start + toMs(packet->duration, stream->time_base);
if (packet->duration <= 0) {
end = subtitle.end_display_time > 0 && subtitle.end_display_time != UINT32_MAX ? start + subtitle.end_display_time : start + 3000;
}
for (unsigned i = 0; i < subtitle.num_rects; ++i) {
const AVSubtitleRect *rect = subtitle.rects[i];
QString text;
if (rect->type == SUBTITLE_ASS && rect->ass) {
text = assRectText(rect->ass);
} else if (rect->type == SUBTITLE_TEXT && rect->text) {
text = QString::fromUtf8(rect->text).trimmed();
}
if (!text.isEmpty()) {
result.cues.append({start, end, text});
}
}
avsubtitle_free(&subtitle);
}
av_packet_unref(packet.get());
}
return result;
}
SourceResult findTranslationSource(const QString &path)
{
AVFormatContext *rawFormat = nullptr;
if (const int ret = avformat_open_input(&rawFormat, path.toUtf8().constData(), nullptr, nullptr); ret < 0) {
return {-1, i18n("Could not open %1: %2", path, ffmpegError(ret))};
}
std::unique_ptr<AVFormatContext, FormatContextDeleter> format(rawFormat);
const auto language = [](const AVStream *stream) {
const AVDictionaryEntry *tag = av_dict_get(stream->metadata, "language", nullptr, 0);
return tag ? QString::fromUtf8(tag->value) : QString();
};
// Track ids start at 1, since 0 means "none"; id - 1 is the stream index.
QList<TrackSelection::Track> subtitles;
QString audioLanguage;
bool audioIsDefault = false;
for (unsigned i = 0; i < format->nb_streams; ++i) {
const AVStream *stream = format->streams[i];
const bool isDefault = stream->disposition & AV_DISPOSITION_DEFAULT;
if (stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
if (audioLanguage.isEmpty() || (isDefault && !audioIsDefault)) {
audioLanguage = language(stream);
audioIsDefault = isDefault;
}
} else if (stream->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE) {
subtitles.append({static_cast<int>(i) + 1,
language(stream),
QString::fromUtf8(avcodec_get_name(stream->codecpar->codec_id)),
bool(stream->disposition & AV_DISPOSITION_FORCED),
isDefault});
}
}
return {TrackSelection::pickTranslationSource(subtitles, {QStringLiteral("en"), audioLanguage}, 0) - 1, {}};
}
}