134 changes: 82 additions & 52 deletions mythtv/libs/libmythtv/captions/textsubtitleparser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
#include <QRunnable>
#include <QFile>
#include <QDataStream>
#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
#include <QTextCodec>
#elif QT_VERSION < QT_VERSION_CHECK(6,3,0)
#include <QStringConverter>
#endif

// MythTV
#include "libmythbase/mthreadpool.h"
Expand Down Expand Up @@ -166,9 +171,19 @@ struct local_buffer_t {
off_t rbuffer_cur;
};

TextSubtitleParser::TextSubtitleParser(SubtitleReader *parent, QString fileName, TextSubtitles *target)
: m_parent(parent), m_target(target), m_fileName(std::move(fileName))
{
m_pkt = av_packet_alloc();
av_new_packet(m_pkt, 4096);
}

TextSubtitleParser::~TextSubtitleParser()
{
avcodec_free_context(&m_decCtx);
avformat_free_context(m_fmtCtx);
av_packet_free(&m_pkt);
m_stream = nullptr;
delete m_loadHelper;
}

Expand Down Expand Up @@ -216,29 +231,35 @@ int64_t TextSubtitleParser::seek_packet(void *opaque, int64_t offset, int whence
return 0;
}

/// \brief Decode a single packet worth of data.
/// \brief Read the next subtitle in the AV stream.
///
/// av_read_frame guarantees that pkt->pts, pkt->dts and pkt->duration
/// are always set to correct values in AVStream.time_base units (and
/// guessed if the format cannot provide them). pkt->pts can be
/// AV_NOPTS_VALUE if the video format has B-frames, so it is better to
/// rely on pkt->dts if you do not decompress the payload.
int TextSubtitleParser::decode(AVPacket *pkt)
int TextSubtitleParser::ReadNextSubtitle(void)
{
// reset buffer
m_pkt->data = m_pkt->buf->data;
m_pkt->size = m_pkt->buf->size;

int ret = av_read_frame(m_fmtCtx, m_pkt);
if (ret < 0)
return ret;

AVSubtitle sub {};
int got_sub_ptr {0};

int ret = avcodec_decode_subtitle2(m_decCtx, &sub, &got_sub_ptr, pkt);
ret = avcodec_decode_subtitle2(m_decCtx, &sub, &got_sub_ptr, m_pkt);
if (ret < 0)
return ret;
if (!got_sub_ptr)
return -1;

sub.start_display_time = av_q2d(m_stream->time_base) * pkt->dts * 1000;
sub.end_display_time = av_q2d(m_stream->time_base) * (pkt->dts + pkt->duration) * 1000;
sub.start_display_time = av_q2d(m_stream->time_base) * m_pkt->dts * 1000;
sub.end_display_time = av_q2d(m_stream->time_base) * (m_pkt->dts + m_pkt->duration) * 1000;

m_parent->AddAVSubtitle(sub, m_decCtx->codec_id == AV_CODEC_ID_XSUB, false);
m_count += 1;
return ret;
}

Expand Down Expand Up @@ -307,95 +328,94 @@ void TextSubtitleParser::LoadSubtitles(bool inBackground)
LOG(VB_VBI, LOG_INFO,
QString("Finished reading %1 subtitle bytes (requested %2)")
.arg(numread).arg(new_len));
bool isUtf8 {false};
auto qba = QByteArray::fromRawData(sub_data.rbuffer_text,
sub_data.rbuffer_len);
#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
QTextCodec *textCodec = QTextCodec::codecForUtfText(qba, nullptr);
isUtf8 = (textCodec != nullptr);
#elif QT_VERSION < QT_VERSION_CHECK(6,3,0)
auto qba_encoding = QStringConverter::encodingForData(qba);
isUtf8 = qba_encoding.has_value
&& (qba_encoding.value == QStringConverter::Utf8);
#else
isUtf8 = qba.isValidUtf8();
#endif

// Create a format context and tie it to the file buffer.
AVFormatContext *fmt_ctx = avformat_alloc_context();
if (fmt_ctx == nullptr) {
m_fmtCtx = avformat_alloc_context();
if (m_fmtCtx == nullptr) {
LOG(VB_VBI, LOG_INFO, "Couldn't allocate format context");
return;
}
auto *avio_ctx_buffer = (uint8_t*)av_malloc(IO_BUFFER_SIZE);
if (avio_ctx_buffer == nullptr)
{
LOG(VB_VBI, LOG_INFO, "Couldn't allocate mamory for avio context");
avformat_free_context(fmt_ctx);
LOG(VB_VBI, LOG_INFO, "Couldn't allocate memory for avio context");
avformat_free_context(m_fmtCtx);
return;
}
fmt_ctx->pb = avio_alloc_context(avio_ctx_buffer, IO_BUFFER_SIZE,
m_fmtCtx->pb = avio_alloc_context(avio_ctx_buffer, IO_BUFFER_SIZE,
0, &sub_data,
&read_packet, nullptr, &seek_packet);
if(int ret = avformat_open_input(&fmt_ctx, nullptr, nullptr, nullptr); ret < 0) {
if (int ret = avformat_open_input(&m_fmtCtx, nullptr, nullptr, nullptr); ret < 0) {
LOG(VB_VBI, LOG_INFO, QString("Couldn't open input context %1")
.arg(av_make_error_stdstring(errbuf,ret)));
// FFmpeg frees context on error.
return;
}

// Find the subtitle stream and its context.
QString encoding {"utf-8"};
if (!m_decCtx)
{
const AVCodec *codec {nullptr};
int stream_num = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_SUBTITLE, -1, -1, &codec, 0);
int stream_num = av_find_best_stream(m_fmtCtx, AVMEDIA_TYPE_SUBTITLE, -1, -1, &codec, 0);
if (stream_num < 0) {
LOG(VB_VBI, LOG_INFO, QString("Couldn't find subtitle stream. %1")
.arg(av_make_error_stdstring(errbuf,stream_num)));
avformat_free_context(fmt_ctx);
avformat_free_context(m_fmtCtx);
return;
}
m_stream = fmt_ctx->streams[stream_num];
m_stream = m_fmtCtx->streams[stream_num];
if (m_stream == nullptr) {
LOG(VB_VBI, LOG_INFO, QString("Stream %1 is null").arg(stream_num));
avformat_free_context(fmt_ctx);
avformat_free_context(m_fmtCtx);
return;
}

// Create a decoder for this subtitle stream context.
m_decCtx = avcodec_alloc_context3(codec);
if (!m_decCtx) {
LOG(VB_VBI, LOG_INFO, QString("Couldn't allocate decoder context"));
avformat_free_context(fmt_ctx);
return;
}
if (avcodec_open2(m_decCtx, codec, nullptr) < 0) {
LOG(VB_VBI, LOG_INFO, QString("Couldn't open decoder context"));
avcodec_free_context(&m_decCtx);
avformat_free_context(fmt_ctx);
avformat_free_context(m_fmtCtx);
return;
}
}

/* decode until eof */
AVPacket *pkt = av_packet_alloc();
av_new_packet(pkt, 4096);
while (av_read_frame(fmt_ctx, pkt) >= 0)
{
int bytes {0};
while ((bytes = decode(pkt)) >= 0)
// Ask FFmpeg to convert subtitles to utf-8.
AVDictionary *dict = nullptr;
if (!isUtf8)
{
pkt->data += bytes;
pkt->size -= bytes;
encoding = gCoreContext->GetSetting("SubtitleCodec", "utf-8");
if (encoding != "utf-8")
{
LOG(VB_VBI, LOG_INFO,
QString("Converting from %1 to utf-8.").arg(encoding));
av_dict_set(&dict, "sub_charenc", qPrintable(encoding), 0);
}
}
if (avcodec_open2(m_decCtx, codec, &dict) < 0) {
LOG(VB_VBI, LOG_INFO,
QString("Couldn't open decoder context for encoding %1").arg(encoding));
avcodec_free_context(&m_decCtx);
avformat_free_context(m_fmtCtx);
return;
}

// reset buffer for next packet
pkt->data = pkt->buf->data;
pkt->size = pkt->buf->size;
}

/* flush the decoder */
pkt->data = nullptr;
pkt->size = 0;
while (decode(pkt) >= 0)
{
}

LOG(VB_GENERAL, LOG_INFO, QString("Loaded %1 %2 subtitles from '%3'")
.arg(m_count)
.arg(m_decCtx->codec->long_name, m_fileName));
LOG(VB_GENERAL, LOG_INFO, QString("Loaded %2 '%3' subtitles from %4")
.arg(encoding, m_decCtx->codec->long_name, m_fileName));
m_target->SetLastLoaded();

av_packet_free(&pkt);
m_stream = nullptr;
avformat_free_context(fmt_ctx);
}

QByteArray TextSubtitleParser::GetSubHeader()
Expand All @@ -405,3 +425,13 @@ QByteArray TextSubtitleParser::GetSubHeader()
return { reinterpret_cast<char*>(m_decCtx->subtitle_header),
m_decCtx->subtitle_header_size };
}

void TextSubtitleParser::SeekFrame(int64_t ts, int flags)
{
if (av_seek_frame(m_fmtCtx, -1, ts, flags) < 0)
{
LOG(VB_PLAYBACK, LOG_INFO,
QString("TextSubtitleParser av_seek_frame(fmtCtx, -1, %1, %2) -- error")
.arg(ts).arg(flags));
}
}
9 changes: 5 additions & 4 deletions mythtv/libs/libmythtv/captions/textsubtitleparser.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,13 @@ class SubtitleLoadHelper;
class TextSubtitleParser
{
public:
TextSubtitleParser(SubtitleReader *parent, QString fileName, TextSubtitles *target)
: m_parent(parent), m_target(target), m_fileName(std::move(fileName)) {};
TextSubtitleParser(SubtitleReader *parent, QString fileName, TextSubtitles *target);
~TextSubtitleParser();
void LoadSubtitles(bool inBackground);
int decode(AVPacket *pkt);
QByteArray GetSubHeader();
void SeekFrame(int64_t ts, int flags);
int ReadNextSubtitle(void);

private:
static int read_packet(void *opaque, uint8_t *buf, int buf_size);
Expand All @@ -106,10 +107,10 @@ class TextSubtitleParser
TextSubtitles *m_target {nullptr};
QString m_fileName;

AVFormatContext *m_fmtCtx {nullptr};
AVCodecContext *m_decCtx {nullptr};
AVStream *m_stream {nullptr};

uint32_t m_count {0};
AVPacket *m_pkt {nullptr};
};

#endif
5 changes: 2 additions & 3 deletions mythtv/libs/libmythtv/dbcheck.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3944,9 +3944,8 @@ static bool doUpgradeTVDatabaseSchema(void)

if (dbver == "1377")
{
DBUpdates updates {
"DELETE FROM settings WHERE value='SubtitleCodec'; ",
};
// Change reverted, but the version number can't be reused.
DBUpdates updates {};
if (!performActualUpdate("MythTV", "DBSchemaVer",
updates, "1378", dbver))
return false;
Expand Down
2 changes: 2 additions & 0 deletions mythtv/libs/libmythtv/decoders/avformatdecoder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,8 @@ bool AvFormatDecoder::DoFastForward(long long desiredFrame, bool discardFrames)
m_getRawFrames = oldrawstate;
return false;
}
if (auto* reader = m_parent->GetSubReader(); reader)
reader->SeekFrame(ts, flags);

int normalframes = 0;

Expand Down
34 changes: 34 additions & 0 deletions mythtv/programs/mythfrontend/globalsettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
#include "libmythbase/mythlogging.h"
#include "libmythbase/mythpower.h"
#include "libmythbase/mythsorthelper.h"
#include "libmythbase/mythsystem.h"
#include "libmythbase/mythtranslation.h"
#include "libmythtv/cardutil.h"
#include "libmythtv/channelgroup.h"
Expand Down Expand Up @@ -1567,6 +1568,38 @@ static HostComboBoxSetting *DecodeVBIFormat()
}
#endif

static HostComboBoxSetting *SubtitleCodec()
{
static const QRegularExpression crlf { "[\r\n]" };
static const QRegularExpression suffix { "(//.*)" };

auto *gc = new HostComboBoxSetting("SubtitleCodec");

gc->setLabel(OSDSettings::tr("Subtitle Codec"));

// Translations are now done via FFmpeg(iconv). Get the list of
// encodings that iconv supports.
QScopedPointer<MythSystem>
cmd(MythSystem::Create({"iconv", "-l"}, kMSStdOut));
cmd->Wait();
QString results = cmd->GetStandardOutputStream()->readAll();
#if QT_VERSION < QT_VERSION_CHECK(5,14,0)
QStringList list = results.toLower().split(crlf, QString::SkipEmptyParts);
#else
QStringList list = results.toLower().split(crlf, Qt::SkipEmptyParts);
#endif
list.replaceInStrings(suffix, "");
list.sort();

for (const auto & codec : qAsConst(list))
{
QString val = QString(codec);
gc->addSelection(val, val, val.toLower() == "utf-8");
}

return gc;
}

static HostComboBoxSetting *ChannelOrdering()
{
auto *gc = new HostComboBoxSetting("ChannelOrdering");
Expand Down Expand Up @@ -4495,6 +4528,7 @@ OSDSettings::OSDSettings()
addChild(PersistentBrowseMode());
addChild(BrowseAllTuners());
addChild(DefaultCCMode());
addChild(SubtitleCodec());

//GroupSetting *cc = new GroupSetting();
//cc->setLabel(tr("Closed Captions"));
Expand Down