MythTV master
textsubtitleparser.cpp
Go to the documentation of this file.
1// -*- Mode: c++ -*-
7// ANSI C
8#include <cstdio>
9#include <cstring>
10#include <climits>
11
12// C++
13#include <algorithm>
14
15// Qt
16#include <QRunnable>
17#include <QFile>
18#include <QDataStream>
19#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
20#include <QTextCodec>
21#elif QT_VERSION < QT_VERSION_CHECK(6,3,0)
22#include <QStringConverter>
23#endif
24
25// MythTV
31#include "libmyth/mythaverror.h"
32
35
36static constexpr uint32_t IO_BUFFER_SIZE { 32768 };
37
38// This background thread helper class is adapted from the
39// RebuildSaver class in mythcommflagplayer.cpp.
40class SubtitleLoadHelper : public QRunnable
41{
42 public:
44 TextSubtitles *target)
45
46 : m_parent(parent), m_target(target)
47 {
48 QMutexLocker locker(&s_lock);
50 }
51
52 void run(void) override // QRunnable
53 {
54 m_parent->LoadSubtitles(false);
55
56 QMutexLocker locker(&s_lock);
58 if (!s_loading[m_target])
59 s_wait.wakeAll();
60 }
61
62 static bool IsLoading(TextSubtitles *target)
63 {
64 QMutexLocker locker(&s_lock);
65 return s_loading[target] != 0U;
66 }
67
68 static void Wait(TextSubtitles *target)
69 {
70 QMutexLocker locker(&s_lock);
71 if (!s_loading[target])
72 return;
73 while (s_wait.wait(&s_lock))
74 {
75 if (!s_loading[target])
76 return;
77 }
78 }
79
80 private:
82 TextSubtitles *m_target { nullptr };
83
84 static QMutex s_lock;
85 static QWaitCondition s_wait;
86 static QHash<TextSubtitles*,uint> s_loading;
87};
89QWaitCondition SubtitleLoadHelper::s_wait;
90QHash<TextSubtitles*,uint> SubtitleLoadHelper::s_loading;
91
92// Work around the fact that RemoteFile doesn't work when the target
93// file is actually local.
95{
96public:
97 explicit RemoteFileWrapper(const QString &filename) {
98 // This test stolen from FileRingBuffer::OpenFile()
99 bool is_local =
100 (!filename.startsWith("/dev")) &&
101 ((filename.startsWith("/")) || QFile::exists(filename));
102 m_isRemote = !is_local;
103 if (m_isRemote)
104 {
105 m_localFile = nullptr;
106 m_remoteFile = new RemoteFile(filename, false, false, 0s);
107 }
108 else
109 {
110 m_remoteFile = nullptr;
111 m_localFile = new QFile(filename);
112 if (!m_localFile->open(QIODevice::ReadOnly))
113 {
114 delete m_localFile;
115 m_localFile = nullptr;
116 }
117 }
118 }
120 delete m_remoteFile;
121 delete m_localFile;
122 }
123
124 RemoteFileWrapper(const RemoteFileWrapper &) = delete; // not copyable
125 RemoteFileWrapper &operator=(const RemoteFileWrapper &) = delete; // not copyable
126
127 bool isOpen(void) const {
128 if (m_isRemote)
129 return m_remoteFile->isOpen();
130 return m_localFile;
131 }
132 long long GetFileSize(void) const {
133 if (m_isRemote)
134 return m_remoteFile->GetFileSize();
135 if (m_localFile)
136 return m_localFile->size();
137 return 0;
138 }
139 int Read(void *data, int size) {
140 if (m_isRemote)
141 return m_remoteFile->Read(data, size);
142 if (m_localFile)
143 {
144 QDataStream stream(m_localFile);
145 return stream.readRawData(static_cast<char*>(data), size);
146 }
147 return 0;
148 }
149private:
153};
154
156{
158}
159
161{
163 QMutexLocker locker(&m_lock);
164 m_lastLoaded = QDateTime::currentDateTimeUtc();
165}
166
172};
173
175 : m_parent(parent),
176 m_target(target),
177 m_fileName(std::move(fileName)),
178 m_pkt(av_packet_alloc())
179{
180}
181
183{
184 avcodec_free_context(&m_decCtx);
185 avformat_free_context(m_fmtCtx);
186 av_packet_free(&m_pkt);
187 m_stream = nullptr;
188 delete m_loadHelper;
189}
190
192int TextSubtitleParser::read_packet(void *opaque, uint8_t *buf, int buf_size)
193{
194 auto *bd = (struct local_buffer_t *)opaque;
195
196 /* copy internal buffer data to buf */
197 off_t remaining = bd->rbuffer_len - bd->rbuffer_cur;
198 if (remaining <= 0)
199 return AVERROR_EOF;
200 buf_size = FFMIN(buf_size, remaining);
201 memcpy(buf, bd->rbuffer_text + bd->rbuffer_cur, buf_size);
202 bd->rbuffer_cur += buf_size;
203
204 return buf_size;
205}
206
208int64_t TextSubtitleParser::seek_packet(void *opaque, int64_t offset, int whence)
209{
210 auto *bd = (struct local_buffer_t *)opaque;
211
212 switch (whence)
213 {
214 case SEEK_CUR:
215 offset = bd->rbuffer_cur + offset;
216 break;
217
218 case SEEK_END:
219 offset = bd->rbuffer_len - offset;
220 break;
221
222 case SEEK_SET:
223 break;
224
225 default:
226 return -1;
227 }
228
229 if ((offset < 0) || (offset > bd->rbuffer_len))
230 return -1;
231 bd->rbuffer_cur = offset;
232 return 0;
233}
234
243{
244
245 int ret = av_read_frame(m_fmtCtx, m_pkt);
246 if (ret < 0)
247 return ret;
248
249 AVSubtitle sub {};
250 int got_sub_ptr {0};
251 ret = avcodec_decode_subtitle2(m_decCtx, &sub, &got_sub_ptr, m_pkt);
252 if (ret < 0)
253 return ret;
254 if (!got_sub_ptr)
255 return -1;
256
257 sub.start_display_time = av_q2d(m_stream->time_base) * m_pkt->dts * 1000;
258 sub.end_display_time = av_q2d(m_stream->time_base) * (m_pkt->dts + m_pkt->duration) * 1000;
259
260 m_parent->AddAVSubtitle(sub, m_decCtx->codec_id == AV_CODEC_ID_XSUB, false, false, true);
261 return ret;
262}
263
265{
266 std::string errbuf;
267
268 if (inBackground)
269 {
271 {
274 start(m_loadHelper, "SubtitleLoadHelper");
275 }
276 return;
277 }
278
279 // External subtitles are now presented as AV Subtitles.
283
284 local_buffer_t sub_data {};
285 RemoteFileWrapper rfile(m_fileName/*, false, false, 0*/);
286
287 LOG(VB_VBI, LOG_INFO,
288 QString("Preparing to load subtitle file %1").arg(m_fileName));
289 if (!rfile.isOpen())
290 {
291 LOG(VB_VBI, LOG_INFO,
292 QString("Failed to load subtitle file %1").arg(m_fileName));
293 return;
294 }
297
298 // Only reload if rfile.GetFileSize() has changed.
299 // RemoteFile::GetFileSize can return -1 on error.
300 off_t new_len = rfile.GetFileSize();
301 if (new_len < 0)
302 {
303 LOG(VB_VBI, LOG_INFO,
304 QString("Failed to get file size for %1").arg(m_fileName));
305 return;
306 }
307
308 if (m_target->GetByteCount() == new_len)
309 {
310 LOG(VB_VBI, LOG_INFO,
311 QString("Filesize unchanged (%1), not reloading subs (%2)")
312 .arg(new_len).arg(m_fileName));
314 return;
315 }
316 LOG(VB_VBI, LOG_INFO,
317 QString("Preparing to read %1 subtitle bytes from %2")
318 .arg(new_len).arg(m_fileName));
319 m_target->SetByteCount(new_len);
320 sub_data.rbuffer_len = new_len;
321 sub_data.rbuffer_text = new char[sub_data.rbuffer_len + 1];
322 sub_data.rbuffer_cur = 0;
323
324 // Slurp the entire file into a buffer.
325 int numread = rfile.Read(sub_data.rbuffer_text, sub_data.rbuffer_len);
326 LOG(VB_VBI, LOG_INFO,
327 QString("Finished reading %1 subtitle bytes (requested %2)")
328 .arg(numread).arg(new_len));
329 bool isUtf8 {false};
330 auto qba = QByteArray::fromRawData(sub_data.rbuffer_text,
331 sub_data.rbuffer_len);
332#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
333 QTextCodec *textCodec = QTextCodec::codecForUtfText(qba, nullptr);
334 isUtf8 = (textCodec != nullptr);
335#elif QT_VERSION < QT_VERSION_CHECK(6,3,0)
336 auto qba_encoding = QStringConverter::encodingForData(qba);
337 isUtf8 = qba_encoding.has_value()
338 && (qba_encoding.value() == QStringConverter::Utf8);
339#else
340 isUtf8 = qba.isValidUtf8();
341#endif
342
343 // Create a format context and tie it to the file buffer.
344 m_fmtCtx = avformat_alloc_context();
345 if (m_fmtCtx == nullptr) {
346 LOG(VB_VBI, LOG_INFO, "Couldn't allocate format context");
347 return;
348 }
349 auto *avio_ctx_buffer = (uint8_t*)av_malloc(IO_BUFFER_SIZE);
350 if (avio_ctx_buffer == nullptr)
351 {
352 LOG(VB_VBI, LOG_INFO, "Couldn't allocate memory for avio context");
353 avformat_free_context(m_fmtCtx);
354 return;
355 }
356 m_fmtCtx->pb = avio_alloc_context(avio_ctx_buffer, IO_BUFFER_SIZE,
357 0, &sub_data,
358 &read_packet, nullptr, &seek_packet);
359 if (int ret = avformat_open_input(&m_fmtCtx, nullptr, nullptr, nullptr); ret < 0) {
360 LOG(VB_VBI, LOG_INFO, QString("Couldn't open input context %1")
361 .arg(av_make_error_stdstring(errbuf,ret)));
362 // FFmpeg frees context on error.
363 return;
364 }
365
366 // Find the subtitle stream and its context.
367 QString encoding {"utf-8"};
368 if (!m_decCtx)
369 {
370 const AVCodec *codec {nullptr};
371 int stream_num = av_find_best_stream(m_fmtCtx, AVMEDIA_TYPE_SUBTITLE, -1, -1, &codec, 0);
372 if (stream_num < 0) {
373 LOG(VB_VBI, LOG_INFO, QString("Couldn't find subtitle stream. %1")
374 .arg(av_make_error_stdstring(errbuf,stream_num)));
375 avformat_free_context(m_fmtCtx);
376 return;
377 }
378 m_stream = m_fmtCtx->streams[stream_num];
379 if (m_stream == nullptr) {
380 LOG(VB_VBI, LOG_INFO, QString("Stream %1 is null").arg(stream_num));
381 avformat_free_context(m_fmtCtx);
382 return;
383 }
384
385 // Create a decoder for this subtitle stream context.
386 m_decCtx = avcodec_alloc_context3(codec);
387 if (!m_decCtx) {
388 LOG(VB_VBI, LOG_INFO, QString("Couldn't allocate decoder context"));
389 avformat_free_context(m_fmtCtx);
390 return;
391 }
392
393 // Ask FFmpeg to convert subtitles to utf-8.
394 AVDictionary *dict = nullptr;
395 if (!isUtf8)
396 {
397 encoding = gCoreContext->GetSetting("SubtitleCodec", "utf-8");
398 if (encoding != "utf-8")
399 {
400 LOG(VB_VBI, LOG_INFO,
401 QString("Converting from %1 to utf-8.").arg(encoding));
402 av_dict_set(&dict, "sub_charenc", qPrintable(encoding), 0);
403 }
404 }
405 if (avcodec_open2(m_decCtx, codec, &dict) < 0) {
406 LOG(VB_VBI, LOG_INFO,
407 QString("Couldn't open decoder context for encoding %1").arg(encoding));
408 avcodec_free_context(&m_decCtx);
409 avformat_free_context(m_fmtCtx);
410 return;
411 }
412 }
413
414 LOG(VB_GENERAL, LOG_INFO, QString("Loaded %2 '%3' subtitles from %4")
415 .arg(encoding, m_decCtx->codec->long_name, m_fileName));
417}
418
420{
421 if (nullptr == m_decCtx)
422 return {};
423 return { reinterpret_cast<char*>(m_decCtx->subtitle_header),
424 m_decCtx->subtitle_header_size };
425}
426
427void TextSubtitleParser::SeekFrame(int64_t ts, int flags)
428{
429 if (av_seek_frame(m_fmtCtx, -1, ts, flags) < 0)
430 {
431 LOG(VB_PLAYBACK, LOG_INFO,
432 QString("TextSubtitleParser av_seek_frame(fmtCtx, -1, %1, %2) -- error")
433 .arg(ts).arg(flags));
434 }
435}
static MThreadPool * globalInstance(void)
QString GetSetting(const QString &key, const QString &defaultval="")
long long GetFileSize(void) const
RemoteFileWrapper & operator=(const RemoteFileWrapper &)=delete
RemoteFileWrapper(const QString &filename)
bool isOpen(void) const
int Read(void *data, int size)
RemoteFileWrapper(const RemoteFileWrapper &)=delete
int Read(void *data, int size)
Definition: remotefile.cpp:938
bool isOpen(void) const
Definition: remotefile.cpp:245
long long GetFileSize(void) const
GetFileSize: returns the remote file's size at the time it was first opened Will query the server in ...
static bool IsLoading(TextSubtitles *target)
TextSubtitles * m_target
static void Wait(TextSubtitles *target)
static QWaitCondition s_wait
TextSubtitleParser * m_parent
static QHash< TextSubtitles *, uint > s_loading
void run(void) override
SubtitleLoadHelper(TextSubtitleParser *parent, TextSubtitles *target)
void EnableRawTextSubtitles(bool enable)
bool AddAVSubtitle(AVSubtitle &subtitle, bool fix_position, bool is_selected_forced_track, bool allow_forced, bool isExternal)
void EnableAVSubtitles(bool enable)
void EnableTextSubtitles(bool enable)
static int64_t seek_packet(void *opaque, int64_t offset, int whence)
Seek in the file buffer.
SubtitleReader * m_parent
TextSubtitleParser(SubtitleReader *parent, QString fileName, TextSubtitles *target)
TextSubtitles * m_target
AVFormatContext * m_fmtCtx
static int read_packet(void *opaque, uint8_t *buf, int buf_size)
Read data from the file buffer into the avio context buffer.
void LoadSubtitles(bool inBackground)
void SeekFrame(int64_t ts, int flags)
AVCodecContext * m_decCtx
int ReadNextSubtitle(void)
Read the next subtitle in the AV stream.
SubtitleLoadHelper * m_loadHelper
void SetHasSubtitles(bool hasSubs)
off_t GetByteCount(void) const
void SetByteCount(off_t count)
QDateTime m_lastLoaded
void TextSubtitlesUpdated()
void SetFilename(const QString &fileName)
void SetLastLoaded(void)
~TextSubtitles() override
QRecursiveMutex m_lock
char * av_make_error_stdstring(std::string &errbuf, int errnum)
A C++ equivalent to av_make_error_string.
Definition: mythaverror.cpp:42
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
#define off_t
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
STL namespace.
bool exists(str path)
Definition: xbmcvfs.py:51
A local buffer that the entire file is slurped into.
static constexpr uint32_t IO_BUFFER_SIZE
TextSubtitles Copyright (c) 2006 by Pekka Jääskeläinen Distributed as part of MythTV under GPL v2 and...