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