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 QMap<TextSubtitles*,uint> s_loading;
78 };
80 QWaitCondition SubtitleLoadHelper::s_wait;
81 QMap<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, 0);
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  QMutexLocker locker(&m_lock);
267  m_lastLoaded = QDateTime::currentDateTimeUtc();
268 }
269 
270 void TextSubtitleParser::LoadSubtitles(const QString &fileName,
271  TextSubtitles &target,
272  bool inBackground)
273 {
274  if (inBackground)
275  {
276  if (!SubtitleLoadHelper::IsLoading(&target))
277  {
279  start(new SubtitleLoadHelper(fileName, &target),
280  "SubtitleLoadHelper");
281  }
282  return;
283  }
284  demux_sputext_t sub_data {};
285  RemoteFileWrapper rfile(fileName/*, false, false, 0*/);
286 
287  LOG(VB_VBI, LOG_INFO,
288  QString("Preparing to load subtitle file (%1)").arg(fileName));
289  if (!rfile.isOpen())
290  {
291  LOG(VB_VBI, LOG_INFO,
292  QString("Failed to load subtitle file (%1)").arg(fileName));
293  return;
294  }
295  target.SetHasSubtitles(true);
296  target.SetFilename(fileName);
297 
298  // Only reload if rfile.GetFileSize() has changed.
299  off_t new_len = rfile.GetFileSize();
300  if (target.GetByteCount() == new_len)
301  {
302  LOG(VB_VBI, LOG_INFO,
303  QString("Filesize unchanged (%1), not reloading subs (%2)")
304  .arg(new_len).arg(fileName));
305  target.SetLastLoaded();
306  return;
307  }
308  LOG(VB_VBI, LOG_INFO,
309  QString("Preparing to read %1 subtitle bytes from %2")
310  .arg(new_len).arg(fileName));
311  target.SetByteCount(new_len);
312  sub_data.rbuffer_len = new_len;
313  sub_data.rbuffer_text = new char[sub_data.rbuffer_len + 1];
314  sub_data.rbuffer_cur = 0;
315  sub_data.errs = 0;
316  int numread = rfile.Read(sub_data.rbuffer_text, sub_data.rbuffer_len);
317  LOG(VB_VBI, LOG_INFO,
318  QString("Finished reading %1 subtitle bytes (requested %2)")
319  .arg(numread).arg(new_len));
320 
321  // try to determine the text codec
322  QByteArray test(sub_data.rbuffer_text, sub_data.rbuffer_len);
323  QTextCodec *textCodec = QTextCodec::codecForUtfText(test, nullptr);
324  if (!textCodec)
325  {
326  LOG(VB_VBI, LOG_WARNING, "Failed to autodetect a UTF encoding.");
327  QString codec = gCoreContext->GetSetting("SubtitleCodec", "");
328  if (!codec.isEmpty())
329  textCodec = QTextCodec::codecForName(codec.toLatin1());
330  if (!textCodec)
331  textCodec = QTextCodec::codecForName("utf-8");
332  if (!textCodec)
333  {
334  LOG(VB_VBI, LOG_ERR,
335  QString("Failed to find codec for subtitle file '%1'")
336  .arg(fileName));
337  return;
338  }
339  }
340 
341  LOG(VB_VBI, LOG_INFO, QString("Opened subtitle file '%1' with codec '%2'")
342  .arg(fileName).arg(textCodec->name().constData()));
343 
344  // load the entire subtitle file, converting to unicode as we go
345  QScopedPointer<QTextDecoder> dec(textCodec->makeDecoder());
346  QString data = dec->toUnicode(sub_data.rbuffer_text, sub_data.rbuffer_len);
347  if (data.isEmpty())
348  {
349  LOG(VB_VBI, LOG_WARNING,
350  QString("Data loaded from subtitle file '%1' is empty.")
351  .arg(fileName));
352  return;
353  }
354 
355  // convert back to utf-8 for parsing
356  QByteArray ba = data.toUtf8();
357  delete[] sub_data.rbuffer_text;
358  sub_data.rbuffer_text = ba.data();
359  sub_data.rbuffer_len = ba.size();
360 
361  subtitle_t *loaded_subs = sub_read_file(&sub_data);
362  if (!loaded_subs)
363  {
364  // Don't delete[] sub_data.rbuffer_text; because the
365  // QByteArray destructor will clean up.
366  LOG(VB_VBI, LOG_ERR, QString("Failed to read subtitles from '%1'")
367  .arg(fileName));
368  return;
369  }
370 
371  LOG(VB_VBI, LOG_INFO, QString("Found %1 subtitles in file '%2'")
372  .arg(sub_data.num).arg(fileName));
373  target.SetFrameBasedTiming(sub_data.uses_time == 0);
374  target.Clear();
375 
376  // convert the subtitles to our own format, free the original structures
377  // and convert back to unicode
378  textCodec = QTextCodec::codecForName("utf-8");
379  if (textCodec)
380  dec.reset(textCodec->makeDecoder());
381 
382  for (int sub_i = 0; sub_i < sub_data.num; ++sub_i)
383  {
384  const subtitle_t *sub = &loaded_subs[sub_i];
385  text_subtitle_t newsub(sub->start, sub->end);
386 
387  if (!target.IsFrameBasedTiming())
388  {
389  newsub.m_start *= 10; // convert from csec to msec
390  newsub.m_end *= 10;
391  }
392 
393  for (int line = 0; line < sub->lines; ++line)
394  {
395  const char *subLine = sub->text[line];
396  QString str;
397  if (textCodec)
398  str = dec->toUnicode(subLine, strlen(subLine));
399  else
400  str = QString(subLine);
401  newsub.m_textLines.push_back(str);
402 
403  free(sub->text[line]);
404  }
405  target.AddSubtitle(newsub);
406  }
407 
408  // textCodec object is managed by Qt, do not delete...
409 
410  free(loaded_subs);
411  // Don't delete[] sub_data.rbuffer_text; because the QByteArray
412  // destructor will clean up.
413 
414  target.SetLastLoaded();
415 }
static QWaitCondition s_wait
static QMap< TextSubtitles *, uint > s_loading
QStringList m_textLines
long end
Ending time in msec or starting frame.
static bool IsLoading(TextSubtitles *target)
RemoteFileWrapper & operator=(const RemoteFileWrapper &)=delete
int lines
Count of text lines in this subtitle set.
long long GetFileSize(void) const
subtitle_t * sub_read_file(demux_sputext_t *demuxstr)
void run(void) override
text_subtitle_t m_lastReturnedSubtitle
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
#define off_t
void AddSubtitle(const text_subtitle_t &newSub)
void SetFilename(const QString &fileName)
off_t GetByteCount(void) const
static void LoadSubtitles(const QString &fileName, TextSubtitles &target, bool inBackground)
static void Wait(TextSubtitles *target)
QString GetSetting(const QString &key, const QString &defaultval="")
void SetByteCount(off_t count)
TextSubtitleList m_subtitles
int Read(void *data, int size)
Definition: remotefile.cpp:936
void SetHasSubtitles(bool hasSubs)
uint64_t m_end
Ending time in msec or ending frame.
static bool operator<(const text_subtitle_t &left, const text_subtitle_t &right)
long long GetFileSize(void) const
GetFileSize: returns the remote file's size at the time it was first opened Will query the server in ...
bool isOpen(void) const
static MThreadPool * globalInstance(void)
const QString & m_fileName
void SetLastLoaded(void)
TextSubtitles * m_target
bool isOpen(void) const
Definition: remotefile.cpp:246
bool HasSubtitleChanged(uint64_t timecode) const
Returns true in case the subtitle to display has changed since the last GetSubtitles() call.
RemoteFileWrapper(const QString &filename)
char * text[SUB_MAX_TEXT]
The subtitle text lines.
QDateTime m_lastLoaded
QStringList GetSubtitles(uint64_t timecode)
Returns the subtitles to display at the given timecode.
int Read(void *data, int size)
uint64_t m_start
Starting time in msec or starting frame.
long start
Starting time in msec or starting frame.
bool IsFrameBasedTiming(void) const
Returns true in case the subtitle timing data is frame-based.
void SetFrameBasedTiming(bool frameBasedTiming)
SubtitleLoadHelper(const QString &fileName, TextSubtitles *target)
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:23