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 using std::lower_bound;
15 
16 // Qt
17 #include <QRunnable>
18 #include <QTextCodec>
19 #include <QFile>
20 #include <QDataStream>
21 
22 // MythTV
23 #include "mythcorecontext.h"
24 #include "remotefile.h"
27 #include "mythlogging.h"
28 #include "mthreadpool.h"
29 
30 // This background thread helper class is adapted from the
31 // RebuildSaver class in mythcommflagplayer.cpp.
32 class SubtitleLoadHelper : public QRunnable
33 {
34  public:
35  SubtitleLoadHelper(const QString &fileName,
36  TextSubtitles *target)
37  : m_fileName(fileName), m_target(target)
38  {
39  QMutexLocker locker(&s_lock);
41  }
42 
43  void run(void) override // QRunnable
44  {
46 
47  QMutexLocker locker(&s_lock);
49  if (!s_loading[m_target])
50  s_wait.wakeAll();
51  }
52 
53  static bool IsLoading(TextSubtitles *target)
54  {
55  QMutexLocker locker(&s_lock);
56  return s_loading[target] != 0U;
57  }
58 
59  static void Wait(TextSubtitles *target)
60  {
61  QMutexLocker locker(&s_lock);
62  if (!s_loading[target])
63  return;
64  while (s_wait.wait(&s_lock))
65  {
66  if (!s_loading[target])
67  return;
68  }
69  }
70 
71  private:
72  const QString &m_fileName;
74 
75  static QMutex s_lock;
76  static QWaitCondition s_wait;
77  static QHash<TextSubtitles*,uint> s_loading;
78 };
80 QWaitCondition SubtitleLoadHelper::s_wait;
81 QHash<TextSubtitles*,uint> SubtitleLoadHelper::s_loading;
82 
83 // Work around the fact that RemoteFile doesn't work when the target
84 // file is actually local.
86 {
87 public:
88  explicit RemoteFileWrapper(const QString &filename) {
89  // This test stolen from FileRingBuffer::OpenFile()
90  bool is_local =
91  (!filename.startsWith("/dev")) &&
92  ((filename.startsWith("/")) || QFile::exists(filename));
93  m_isRemote = !is_local;
94  if (m_isRemote)
95  {
96  m_localFile = nullptr;
97  m_remoteFile = new RemoteFile(filename, false, false, 0s);
98  }
99  else
100  {
101  m_remoteFile = nullptr;
102  m_localFile = new QFile(filename);
103  if (!m_localFile->open(QIODevice::ReadOnly))
104  {
105  delete m_localFile;
106  m_localFile = nullptr;
107  }
108  }
109  }
111  delete m_remoteFile;
112  delete m_localFile;
113  }
114 
115  RemoteFileWrapper(const RemoteFileWrapper &) = delete; // not copyable
116  RemoteFileWrapper &operator=(const RemoteFileWrapper &) = delete; // not copyable
117 
118  bool isOpen(void) const {
119  if (m_isRemote)
120  return m_remoteFile->isOpen();
121  return m_localFile;
122  }
123  long long GetFileSize(void) const {
124  if (m_isRemote)
125  return m_remoteFile->GetFileSize();
126  if (m_localFile)
127  return m_localFile->size();
128  return 0;
129  }
130  int Read(void *data, int size) {
131  if (m_isRemote)
132  return m_remoteFile->Read(data, size);
133  if (m_localFile)
134  {
135  QDataStream stream(m_localFile);
136  return stream.readRawData(static_cast<char*>(data), size);
137  }
138  return 0;
139  }
140 private:
143  QFile *m_localFile;
144 };
145 
146 static bool operator<(const text_subtitle_t& left,
147  const text_subtitle_t& right)
148 {
149  return left.m_start < right.m_start;
150 }
151 
153 {
155 }
156 
167 bool TextSubtitles::HasSubtitleChanged(uint64_t timecode) const
168 {
169  return (timecode < m_lastReturnedSubtitle.m_start ||
170  timecode > m_lastReturnedSubtitle.m_end);
171 }
172 
180 QStringList TextSubtitles::GetSubtitles(uint64_t timecode)
181 {
182  QStringList list;
183  if (!m_isInProgress && m_subtitles.empty())
184  return list;
185 
186  text_subtitle_t searchTarget(timecode, timecode);
187 
188  auto nextSubPos =
189  lower_bound(m_subtitles.begin(), m_subtitles.end(), searchTarget);
190 
191  uint64_t startCode = 0;
192  uint64_t endCode = 0;
193  if (nextSubPos != m_subtitles.begin())
194  {
195  auto currentSubPos = nextSubPos;
196  --currentSubPos;
197 
198  const text_subtitle_t &sub = *currentSubPos;
199  if (sub.m_start <= timecode && sub.m_end >= timecode)
200  {
201  // found a sub to display
204  }
205 
206  // the subtitle time span has ended, let's display a blank sub
207  startCode = sub.m_end + 1;
208  }
209 
210  if (nextSubPos == m_subtitles.end())
211  {
212  if (m_isInProgress)
213  {
214  const int maxReloadInterval = 1000; // ms
215  if (IsFrameBasedTiming())
216  {
217  // Assume conservative 24fps
218  endCode = startCode + maxReloadInterval / 24;
219  }
220  else
221  {
222  endCode = startCode + maxReloadInterval;
223  }
224  QDateTime now = QDateTime::currentDateTimeUtc();
225  if (!m_fileName.isEmpty() &&
226  m_lastLoaded.msecsTo(now) >= maxReloadInterval)
227  {
229  }
230  }
231  else
232  {
233  // at the end of video, the blank subtitle should last
234  // until forever
235  endCode = startCode + INT_MAX;
236  }
237  }
238  else
239  {
240  endCode = (*nextSubPos).m_start - 1;
241  }
242 
243  // we are in a position in which there are no subtitles to display,
244  // return an empty subtitle and create a dummy empty subtitle for this
245  // time span so SubtitleChanged() functions also in this case
246  text_subtitle_t blankSub(startCode, endCode);
247  m_lastReturnedSubtitle = blankSub;
248 
249  return list;
250 }
251 
253 {
254  QMutexLocker locker(&m_lock);
255  m_subtitles.push_back(newSub);
256 }
257 
259 {
260  QMutexLocker locker(&m_lock);
261  m_subtitles.clear();
262 }
263 
265 {
266  emit TextSubtitlesUpdated();
267  QMutexLocker locker(&m_lock);
268  m_lastLoaded = QDateTime::currentDateTimeUtc();
269 }
270 
271 void TextSubtitleParser::LoadSubtitles(const QString &fileName,
272  TextSubtitles &target,
273  bool inBackground)
274 {
275  if (inBackground)
276  {
277  if (!SubtitleLoadHelper::IsLoading(&target))
278  {
280  start(new SubtitleLoadHelper(fileName, &target),
281  "SubtitleLoadHelper");
282  }
283  return;
284  }
285  demux_sputext_t sub_data {};
286  RemoteFileWrapper rfile(fileName/*, false, false, 0*/);
287 
288  LOG(VB_VBI, LOG_INFO,
289  QString("Preparing to load subtitle file (%1)").arg(fileName));
290  if (!rfile.isOpen())
291  {
292  LOG(VB_VBI, LOG_INFO,
293  QString("Failed to load subtitle file (%1)").arg(fileName));
294  return;
295  }
296  target.SetHasSubtitles(true);
297  target.SetFilename(fileName);
298 
299  // Only reload if rfile.GetFileSize() has changed.
300  off_t new_len = rfile.GetFileSize();
301  if (target.GetByteCount() == new_len)
302  {
303  LOG(VB_VBI, LOG_INFO,
304  QString("Filesize unchanged (%1), not reloading subs (%2)")
305  .arg(new_len).arg(fileName));
306  target.SetLastLoaded();
307  return;
308  }
309  LOG(VB_VBI, LOG_INFO,
310  QString("Preparing to read %1 subtitle bytes from %2")
311  .arg(new_len).arg(fileName));
312  target.SetByteCount(new_len);
313  sub_data.rbuffer_len = new_len;
314  sub_data.rbuffer_text = new char[sub_data.rbuffer_len + 1];
315  sub_data.rbuffer_cur = 0;
316  sub_data.errs = 0;
317  int numread = rfile.Read(sub_data.rbuffer_text, sub_data.rbuffer_len);
318  LOG(VB_VBI, LOG_INFO,
319  QString("Finished reading %1 subtitle bytes (requested %2)")
320  .arg(numread).arg(new_len));
321 
322  // try to determine the text codec
323  QByteArray test(sub_data.rbuffer_text, sub_data.rbuffer_len);
324  QTextCodec *textCodec = QTextCodec::codecForUtfText(test, nullptr);
325  if (!textCodec)
326  {
327  LOG(VB_VBI, LOG_WARNING, "Failed to autodetect a UTF encoding.");
328  QString codec = gCoreContext->GetSetting("SubtitleCodec", "");
329  if (!codec.isEmpty())
330  textCodec = QTextCodec::codecForName(codec.toLatin1());
331  if (!textCodec)
332  textCodec = QTextCodec::codecForName("utf-8");
333  if (!textCodec)
334  {
335  LOG(VB_VBI, LOG_ERR,
336  QString("Failed to find codec for subtitle file '%1'")
337  .arg(fileName));
338  return;
339  }
340  }
341 
342  LOG(VB_VBI, LOG_INFO, QString("Opened subtitle file '%1' with codec '%2'")
343  .arg(fileName, textCodec->name().constData()));
344 
345  // load the entire subtitle file, converting to unicode as we go
346  QScopedPointer<QTextDecoder> dec(textCodec->makeDecoder());
347  QString data = dec->toUnicode(sub_data.rbuffer_text, sub_data.rbuffer_len);
348  if (data.isEmpty())
349  {
350  LOG(VB_VBI, LOG_WARNING,
351  QString("Data loaded from subtitle file '%1' is empty.")
352  .arg(fileName));
353  return;
354  }
355 
356  // convert back to utf-8 for parsing
357  QByteArray ba = data.toUtf8();
358  delete[] sub_data.rbuffer_text;
359  sub_data.rbuffer_text = ba.data();
360  sub_data.rbuffer_len = ba.size();
361 
362  try
363  {
364  if (!sub_read_file(&sub_data))
365  {
366  // Don't delete[] sub_data.rbuffer_text; because the
367  // QByteArray destructor will clean up.
368  LOG(VB_VBI, LOG_ERR, QString("Failed to read subtitles from '%1'")
369  .arg(fileName));
370  return;
371  }
372  } catch (std::exception& e) {
373  LOG(VB_VBI, LOG_ERR,
374  QString("Exception reading subtitles file (%1)").arg(e.what()));
375  return;
376  }
377 
378  LOG(VB_VBI, LOG_INFO, QString("Found %1 subtitles in file '%2'")
379  .arg(sub_data.num).arg(fileName));
380  target.SetFrameBasedTiming(sub_data.uses_time == 0);
381  target.Clear();
382 
383  // convert the subtitles to our own format, free the original structures
384  // and convert back to unicode
385  textCodec = QTextCodec::codecForName("utf-8");
386  if (textCodec)
387  dec.reset(textCodec->makeDecoder());
388 
389  for (const auto& sub : sub_data.subtitles)
390  {
391  text_subtitle_t newsub(sub.start, sub.end);
392 
393  if (!target.IsFrameBasedTiming())
394  {
395  newsub.m_start *= 10; // convert from csec to msec
396  newsub.m_end *= 10;
397  }
398 
399  for (const auto & line : sub.text)
400  {
401  const char *subLine = line.c_str();
402  QString str;
403  if (textCodec)
404  str = dec->toUnicode(subLine, strlen(subLine));
405  else
406  str = QString(subLine);
407  newsub.m_textLines.push_back(str);
408  }
409  target.AddSubtitle(newsub);
410  }
411 
412  // textCodec object is managed by Qt, do not delete...
413 
414  // Don't delete[] sub_data.rbuffer_text; because the QByteArray
415  // destructor will clean up.
416 
417  LOG(VB_GENERAL, LOG_INFO, QString("Loaded %1 subtitles from '%2'")
418  .arg(target.GetSubtitleCount()).arg(fileName));
419  target.SetLastLoaded();
420 }
text_subtitle_t::m_start
uint64_t m_start
Starting time in msec or starting frame.
Definition: textsubtitleparser.h:29
sub_read_file
bool sub_read_file(demux_sputext_t *demuxstr)
Definition: xine_demux_sputext.cpp:1023
RemoteFileWrapper::GetFileSize
long long GetFileSize(void) const
Definition: textsubtitleparser.cpp:123
RemoteFileWrapper::m_remoteFile
RemoteFile * m_remoteFile
Definition: textsubtitleparser.cpp:142
TextSubtitles::AddSubtitle
void AddSubtitle(const text_subtitle_t &newSub)
Definition: textsubtitleparser.cpp:252
RemoteFile::GetFileSize
long long GetFileSize(void) const
GetFileSize: returns the remote file's size at the time it was first opened Will query the server in ...
Definition: remotefile.cpp:1091
textsubtitleparser.h
operator<
static bool operator<(const text_subtitle_t &left, const text_subtitle_t &right)
Definition: textsubtitleparser.cpp:146
text_subtitle_t
TextSubtitles Copyright (c) 2006 by Pekka Jääskeläinen Distributed as part of MythTV under GPL v2 and lat...
Definition: textsubtitleparser.h:20
TextSubtitles::m_lastLoaded
QDateTime m_lastLoaded
Definition: textsubtitleparser.h:101
demux_sputext_t
Definition: xine_demux_sputext.h:37
RemoteFileWrapper::operator=
RemoteFileWrapper & operator=(const RemoteFileWrapper &)=delete
SubtitleLoadHelper::m_fileName
const QString & m_fileName
Definition: textsubtitleparser.cpp:72
text_subtitle_t::m_textLines
QStringList m_textLines
Definition: textsubtitleparser.h:31
TextSubtitles::HasSubtitleChanged
bool HasSubtitleChanged(uint64_t timecode) const
Returns true in case the subtitle to display has changed since the last GetSubtitles() call.
Definition: textsubtitleparser.cpp:167
SubtitleLoadHelper::SubtitleLoadHelper
SubtitleLoadHelper(const QString &fileName, TextSubtitles *target)
Definition: textsubtitleparser.cpp:35
TextSubtitles::SetByteCount
void SetByteCount(off_t count)
Definition: textsubtitleparser.h:78
RemoteFile
Definition: remotefile.h:17
TextSubtitles::Clear
void Clear(void)
Definition: textsubtitleparser.cpp:258
LOG
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:23
RemoteFile::isOpen
bool isOpen(void) const
Definition: remotefile.cpp:246
TextSubtitles::~TextSubtitles
~TextSubtitles() override
Definition: textsubtitleparser.cpp:152
TextSubtitles::GetSubtitles
QStringList GetSubtitles(uint64_t timecode)
Returns the subtitles to display at the given timecode.
Definition: textsubtitleparser.cpp:180
RemoteFileWrapper::m_isRemote
bool m_isRemote
Definition: textsubtitleparser.cpp:141
SubtitleLoadHelper
Definition: textsubtitleparser.cpp:32
mythlogging.h
text_subtitle_t::m_end
uint64_t m_end
Ending time in msec or ending frame.
Definition: textsubtitleparser.h:30
remotefile.h
RemoteFileWrapper::m_localFile
QFile * m_localFile
Definition: textsubtitleparser.cpp:143
RemoteFileWrapper
Definition: textsubtitleparser.cpp:85
SubtitleLoadHelper::s_wait
static QWaitCondition s_wait
Definition: textsubtitleparser.cpp:76
TextSubtitles
Definition: textsubtitleparser.h:36
xine_demux_sputext.h
gCoreContext
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
Definition: mythcorecontext.cpp:60
SubtitleLoadHelper::s_lock
static QMutex s_lock
Definition: textsubtitleparser.cpp:75
TextSubtitles::GetSubtitleCount
int GetSubtitleCount(void) const
Definition: textsubtitleparser.h:90
SubtitleLoadHelper::s_loading
static QHash< TextSubtitles *, uint > s_loading
Definition: textsubtitleparser.cpp:77
off_t
#define off_t
Definition: mythiowrapper.cpp:240
mthreadpool.h
TextSubtitles::SetHasSubtitles
void SetHasSubtitles(bool hasSubs)
Definition: textsubtitleparser.h:87
RemoteFileWrapper::Read
int Read(void *data, int size)
Definition: textsubtitleparser.cpp:130
SubtitleLoadHelper::IsLoading
static bool IsLoading(TextSubtitles *target)
Definition: textsubtitleparser.cpp:53
TextSubtitles::GetByteCount
off_t GetByteCount(void) const
Definition: textsubtitleparser.h:82
RemoteFileWrapper::~RemoteFileWrapper
~RemoteFileWrapper()
Definition: textsubtitleparser.cpp:110
TextSubtitles::m_lastReturnedSubtitle
text_subtitle_t m_lastReturnedSubtitle
Definition: textsubtitleparser.h:98
mythcorecontext.h
TextSubtitles::m_fileName
QString m_fileName
Definition: textsubtitleparser.h:100
SubtitleLoadHelper::run
void run(void) override
Definition: textsubtitleparser.cpp:43
TextSubtitles::SetLastLoaded
void SetLastLoaded(void)
Definition: textsubtitleparser.cpp:264
TextSubtitles::IsFrameBasedTiming
bool IsFrameBasedTiming(void) const
Returns true in case the subtitle timing data is frame-based.
Definition: textsubtitleparser.h:62
TextSubtitles::m_isInProgress
bool m_isInProgress
Definition: textsubtitleparser.h:105
RemoteFileWrapper::isOpen
bool isOpen(void) const
Definition: textsubtitleparser.cpp:118
RemoteFileWrapper::RemoteFileWrapper
RemoteFileWrapper(const QString &filename)
Definition: textsubtitleparser.cpp:88
TextSubtitles::m_subtitles
TextSubtitleList m_subtitles
Definition: textsubtitleparser.h:97
TextSubtitles::SetFrameBasedTiming
void SetFrameBasedTiming(bool frameBasedTiming)
Definition: textsubtitleparser.h:65
TextSubtitles::TextSubtitlesUpdated
void TextSubtitlesUpdated()
TextSubtitles::SetFilename
void SetFilename(const QString &fileName)
Definition: textsubtitleparser.h:70
SubtitleLoadHelper::m_target
TextSubtitles * m_target
Definition: textsubtitleparser.cpp:73
build_compdb.filename
filename
Definition: build_compdb.py:21
MThreadPool::globalInstance
static MThreadPool * globalInstance(void)
Definition: mthreadpool.cpp:317
SubtitleLoadHelper::Wait
static void Wait(TextSubtitles *target)
Definition: textsubtitleparser.cpp:59
TextSubtitleParser::LoadSubtitles
static void LoadSubtitles(const QString &fileName, TextSubtitles &target, bool inBackground)
Definition: textsubtitleparser.cpp:271
MythCoreContext::GetSetting
QString GetSetting(const QString &key, const QString &defaultval="")
Definition: mythcorecontext.cpp:922
RemoteFile::Read
int Read(void *data, int size)
Definition: remotefile.cpp:936
TextSubtitles::m_lock
QRecursiveMutex m_lock
Definition: textsubtitleparser.h:113