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"
25 #include "textsubtitleparser.h"
26 #include "xine_demux_sputext.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  bool isOpen(void) const {
115  if (m_isRemote)
116  return m_remoteFile->isOpen();
117  return m_localFile;
118  }
119  long long GetFileSize(void) const {
120  if (m_isRemote)
121  return m_remoteFile->GetFileSize();
122  if (m_localFile)
123  return m_localFile->size();
124  return 0;
125  }
126  int Read(void *data, int size) {
127  if (m_isRemote)
128  return m_remoteFile->Read(data, size);
129  if (m_localFile)
130  {
131  QDataStream stream(m_localFile);
132  return stream.readRawData(static_cast<char*>(data), size);
133  }
134  return 0;
135  }
136 private:
137  RemoteFileWrapper(const RemoteFileWrapper &) = delete; // not copyable
138  RemoteFileWrapper &operator=(const RemoteFileWrapper &) = delete; // not copyable
139 
142  QFile *m_localFile;
143 };
144 
145 static bool operator<(const text_subtitle_t& left,
146  const text_subtitle_t& right)
147 {
148  return left.m_start < right.m_start;
149 }
150 
152 {
154 }
155 
166 bool TextSubtitles::HasSubtitleChanged(uint64_t timecode) const
167 {
168  return (timecode < m_lastReturnedSubtitle.m_start ||
169  timecode > m_lastReturnedSubtitle.m_end);
170 }
171 
179 QStringList TextSubtitles::GetSubtitles(uint64_t timecode)
180 {
181  QStringList list;
182  if (!m_isInProgress && m_subtitles.empty())
183  return list;
184 
185  text_subtitle_t searchTarget(timecode, timecode);
186 
187  TextSubtitleList::const_iterator nextSubPos =
188  lower_bound(m_subtitles.begin(), m_subtitles.end(), searchTarget);
189 
190  uint64_t startCode = 0, endCode = 0;
191  if (nextSubPos != m_subtitles.begin())
192  {
193  TextSubtitleList::const_iterator currentSubPos = nextSubPos;
194  --currentSubPos;
195 
196  const text_subtitle_t &sub = *currentSubPos;
197  if (sub.m_start <= timecode && sub.m_end >= timecode)
198  {
199  // found a sub to display
202  }
203 
204  // the subtitle time span has ended, let's display a blank sub
205  startCode = sub.m_end + 1;
206  }
207 
208  if (nextSubPos == m_subtitles.end())
209  {
210  if (m_isInProgress)
211  {
212  const int maxReloadInterval = 1000; // ms
213  if (IsFrameBasedTiming())
214  // Assume conservative 24fps
215  endCode = startCode + maxReloadInterval / 24;
216  else
217  endCode = startCode + maxReloadInterval;
218  QDateTime now = QDateTime::currentDateTimeUtc();
219  if (!m_fileName.isEmpty() &&
220  m_lastLoaded.msecsTo(now) >= maxReloadInterval)
221  {
223  }
224  }
225  else
226  {
227  // at the end of video, the blank subtitle should last
228  // until forever
229  endCode = startCode + INT_MAX;
230  }
231  }
232  else
233  {
234  endCode = (*nextSubPos).m_start - 1;
235  }
236 
237  // we are in a position in which there are no subtitles to display,
238  // return an empty subtitle and create a dummy empty subtitle for this
239  // time span so SubtitleChanged() functions also in this case
240  text_subtitle_t blankSub(startCode, endCode);
241  m_lastReturnedSubtitle = blankSub;
242 
243  return list;
244 }
245 
247 {
248  QMutexLocker locker(&m_lock);
249  m_subtitles.push_back(newSub);
250 }
251 
253 {
254  QMutexLocker locker(&m_lock);
255  m_subtitles.clear();
256 }
257 
259 {
260  QMutexLocker locker(&m_lock);
261  m_lastLoaded = QDateTime::currentDateTimeUtc();
262 }
263 
264 void TextSubtitleParser::LoadSubtitles(const QString &fileName,
265  TextSubtitles &target,
266  bool inBackground)
267 {
268  if (inBackground)
269  {
270  if (!SubtitleLoadHelper::IsLoading(&target))
272  start(new SubtitleLoadHelper(fileName, &target),
273  "SubtitleLoadHelper");
274  return;
275  }
276  demux_sputext_t sub_data;
277  RemoteFileWrapper rfile(fileName/*, false, false, 0*/);
278 
279  LOG(VB_VBI, LOG_INFO,
280  QString("Preparing to load subtitle file (%1)").arg(fileName));
281  if (!rfile.isOpen())
282  {
283  LOG(VB_VBI, LOG_INFO,
284  QString("Failed to load subtitle file (%1)").arg(fileName));
285  return;
286  }
287  target.SetHasSubtitles(true);
288  target.SetFilename(fileName);
289 
290  // Only reload if rfile.GetFileSize() has changed.
291  off_t new_len = rfile.GetFileSize();
292  if (target.GetByteCount() == new_len)
293  {
294  LOG(VB_VBI, LOG_INFO,
295  QString("Filesize unchanged (%1), not reloading subs (%2)")
296  .arg(new_len).arg(fileName));
297  target.SetLastLoaded();
298  return;
299  }
300  LOG(VB_VBI, LOG_INFO,
301  QString("Preparing to read %1 subtitle bytes from %2")
302  .arg(new_len).arg(fileName));
303  target.SetByteCount(new_len);
304  sub_data.rbuffer_len = new_len;
305  sub_data.rbuffer_text = new char[sub_data.rbuffer_len + 1];
306  sub_data.rbuffer_cur = 0;
307  sub_data.errs = 0;
308  int numread = rfile.Read(sub_data.rbuffer_text, sub_data.rbuffer_len);
309  LOG(VB_VBI, LOG_INFO,
310  QString("Finished reading %1 subtitle bytes (requested %2)")
311  .arg(numread).arg(new_len));
312 
313  // try to determine the text codec
314  QByteArray test(sub_data.rbuffer_text, sub_data.rbuffer_len);
315  QTextCodec *textCodec = QTextCodec::codecForUtfText(test, nullptr);
316  if (!textCodec)
317  {
318  LOG(VB_VBI, LOG_WARNING, "Failed to autodetect a UTF encoding.");
319  QString codec = gCoreContext->GetSetting("SubtitleCodec", "");
320  if (!codec.isEmpty())
321  textCodec = QTextCodec::codecForName(codec.toLatin1());
322  if (!textCodec)
323  textCodec = QTextCodec::codecForName("utf-8");
324  if (!textCodec)
325  {
326  LOG(VB_VBI, LOG_ERR,
327  QString("Failed to find codec for subtitle file '%1'")
328  .arg(fileName));
329  return;
330  }
331  }
332 
333  LOG(VB_VBI, LOG_INFO, QString("Opened subtitle file '%1' with codec '%2'")
334  .arg(fileName).arg(textCodec->name().constData()));
335 
336  // load the entire subtitle file, converting to unicode as we go
337  QScopedPointer<QTextDecoder> dec(textCodec->makeDecoder());
338  QString data = dec->toUnicode(sub_data.rbuffer_text, sub_data.rbuffer_len);
339  if (data.isEmpty())
340  {
341  LOG(VB_VBI, LOG_WARNING,
342  QString("Data loaded from subtitle file '%1' is empty.")
343  .arg(fileName));
344  return;
345  }
346 
347  // convert back to utf-8 for parsing
348  QByteArray ba = data.toUtf8();
349  delete[] sub_data.rbuffer_text;
350  sub_data.rbuffer_text = ba.data();
351  sub_data.rbuffer_len = ba.size();
352 
353  subtitle_t *loaded_subs = sub_read_file(&sub_data);
354  if (!loaded_subs)
355  {
356  // Don't delete[] sub_data.rbuffer_text; because the
357  // QByteArray destructor will clean up.
358  LOG(VB_VBI, LOG_ERR, QString("Failed to read subtitles from '%1'")
359  .arg(fileName));
360  return;
361  }
362 
363  LOG(VB_VBI, LOG_INFO, QString("Found %1 subtitles in file '%2'")
364  .arg(sub_data.num).arg(fileName));
365  target.SetFrameBasedTiming(sub_data.uses_time == 0);
366  target.Clear();
367 
368  // convert the subtitles to our own format, free the original structures
369  // and convert back to unicode
370  textCodec = QTextCodec::codecForName("utf-8");
371  if (textCodec)
372  dec.reset(textCodec->makeDecoder());
373 
374  for (int sub_i = 0; sub_i < sub_data.num; ++sub_i)
375  {
376  const subtitle_t *sub = &loaded_subs[sub_i];
377  text_subtitle_t newsub(sub->start, sub->end);
378 
379  if (!target.IsFrameBasedTiming())
380  {
381  newsub.m_start *= 10; // convert from csec to msec
382  newsub.m_end *= 10;
383  }
384 
385  for (int line = 0; line < sub->lines; ++line)
386  {
387  const char *subLine = sub->text[line];
388  QString str;
389  if (textCodec)
390  str = dec->toUnicode(subLine, strlen(subLine));
391  else
392  str = QString(subLine);
393  newsub.m_textLines.push_back(str);
394 
395  free(sub->text[line]);
396  }
397  target.AddSubtitle(newsub);
398  }
399 
400  // textCodec object is managed by Qt, do not delete...
401 
402  free(loaded_subs);
403  // Don't delete[] sub_data.rbuffer_text; because the QByteArray
404  // destructor will clean up.
405 
406  target.SetLastLoaded();
407 }
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
#define LOG(_MASK_, _LEVEL_, _STRING_)
Definition: mythlogging.h:41
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)