MythTV  master
previewgeneratorqueue.cpp
Go to the documentation of this file.
1 
3 
4 // C++
5 #include <algorithm>
6 using std::max;
7 
8 // QT
9 #include <QCoreApplication>
10 #include <QFileInfo>
11 
12 // libmythbase
13 #include "mythcorecontext.h"
14 #include "mythlogging.h"
15 #include "mythdirs.h"
16 #include "mthread.h"
17 
18 // libmyth
19 #include "mythcontext.h"
20 #include "remoteutil.h"
21 
22 // libmythtv
23 #include "previewgenerator.h"
24 
25 #define LOC QString("PreviewQueue: ")
26 
28 
45  uint maxAttempts, uint minBlockSeconds)
46 {
47  s_pgq = new PreviewGeneratorQueue(mode, maxAttempts, minBlockSeconds);
48 }
49 
57 {
58  s_pgq->exit(0);
59  s_pgq->wait();
60  delete s_pgq;
61  s_pgq = nullptr;
62 }
63 
64 /*
65  * Create the queue object for holding preview generators.
66  *
67  * Create the singleton queue of preview generators. This should be
68  * called once at program start-up. All generation requests on this
69  * queue will will be created with the maxAttempts and minBlockSeconds
70  * parameters supplied here.
71  *
72  * \param[in] mode Local or Remote (or both)
73  * \param[in] maxAttempts How many times total will the code attempt
74  * to generate a preview for a specific file, before giving
75  * up and ignoring all future requests.
76  * \param[in] minBlockSeconds How long after a failed preview
77  * generation attempt will the code ignore subsequent
78  * requests.
79  *
80  * \note Never call this routine directly. Call the
81  * CreatePreviewGeneratorQueue function instead.
82  */
85  uint maxAttempts, uint minBlockSeconds) :
86  MThread("PreviewGeneratorQueue"),
87  m_mode(mode),
88  m_maxAttempts(maxAttempts), m_minBlockSeconds(minBlockSeconds)
89 {
90  if (PreviewGenerator::kLocal & mode)
91  {
92  int idealThreads = QThread::idealThreadCount();
93  m_maxThreads = (idealThreads >= 1) ? idealThreads * 2 : 2;
94  }
95 
96  moveToThread(qthread());
97  start();
98 }
99 
109 {
110  // disconnect preview generators
111  QMutexLocker locker(&m_lock);
112  PreviewMap::iterator it = m_previewMap.begin();
113  for (;it != m_previewMap.end(); ++it)
114  {
115  if ((*it).m_gen)
116  (*it).m_gen->deleteLater();
117  (*it).m_gen = nullptr;
118  }
119  locker.unlock();
120  wait();
121 }
122 
145  const ProgramInfo &pginfo,
146  const QSize &outputsize,
147  const QString &outputfile,
148  long long time, bool in_seconds,
149  const QString& token)
150 {
151  if (!s_pgq)
152  return;
153 
154  if (pginfo.GetPathname().isEmpty() ||
155  pginfo.GetBasename() == pginfo.GetPathname())
156  {
157  return;
158  }
159 
160  if (gCoreContext->GetNumSetting("JobAllowPreview", 1) == 0)
161  return;
162 
163  QStringList extra;
164  pginfo.ToStringList(extra);
165  extra += token;
166  extra += QString::number(outputsize.width());
167  extra += QString::number(outputsize.height());
168  extra += outputfile;
169  extra += QString::number(time);
170  extra += (in_seconds ? "1" : "0");
171  MythEvent *e = new MythEvent("GET_PREVIEW", extra);
172  QCoreApplication::postEvent(s_pgq, e);
173 }
174 
182 void PreviewGeneratorQueue::AddListener(QObject *listener)
183 {
184  if (!s_pgq)
185  return;
186 
187  QMutexLocker locker(&s_pgq->m_lock);
188  s_pgq->m_listeners.insert(listener);
189 }
190 
199 {
200  if (!s_pgq)
201  return;
202 
203  QMutexLocker locker(&s_pgq->m_lock);
204  s_pgq->m_listeners.remove(listener);
205 }
206 
224 {
225  if (e->type() != MythEvent::MythEventMessage)
226  return QObject::event(e);
227 
228  MythEvent *me = static_cast<MythEvent*>(e);
229  if (me->Message() == "GET_PREVIEW")
230  {
231  const QStringList &list = me->ExtraDataList();
232  QStringList::const_iterator it = list.begin();
233  ProgramInfo evinfo(it, list.end());
234  QString token;
235  QSize outputsize;
236  QString outputfile;
237  long long time = -1LL;
238  if (it != list.end())
239  token = (*it++);
240  if (it != list.end())
241  outputsize.setWidth((*it++).toInt());
242  if (it != list.end())
243  outputsize.setHeight((*it++).toInt());
244  if (it != list.end())
245  outputfile = (*it++);
246  if (it != list.end())
247  time = (*it++).toLongLong();
248  if (it != list.end())
249  {
250  bool time_fmt_sec = (*it++).toInt() != 0;
251  GeneratePreviewImage(evinfo, outputsize, outputfile,
252  time, time_fmt_sec, token);
253  }
254  return true;
255  }
256  if (me->Message() == "PREVIEW_SUCCESS" ||
257  me->Message() == "PREVIEW_FAILED")
258  {
259  uint recordedingID = me->ExtraData(0).toUInt(); // pginfo->GetRecordingID()
260  const QString& filename = me->ExtraData(1); // outFileName
261  const QString& msg = me->ExtraData(2);
262  const QString& datetime = me->ExtraData(3);
263  const QString& token = me->ExtraData(4);
264 
265  {
266  QMutexLocker locker(&m_lock);
267  QMap<QString,QString>::iterator kit = m_tokenToKeyMap.find(token);
268  if (kit == m_tokenToKeyMap.end())
269  {
270  LOG(VB_GENERAL, LOG_ERR, LOC +
271  QString("Failed to find token %1 in map.").arg(token));
272  return true;
273  }
274  PreviewMap::iterator it = m_previewMap.find(*kit);
275  if (it == m_previewMap.end())
276  {
277  LOG(VB_GENERAL, LOG_ERR, LOC +
278  QString("Failed to find key %1 in map.").arg(*kit));
279  return true;
280  }
281 
282  if ((*it).m_gen)
283  (*it).m_gen->deleteLater();
284  (*it).m_gen = nullptr;
285  (*it).m_genStarted = false;
286  if (me->Message() == "PREVIEW_SUCCESS")
287  {
288  (*it).m_attempts = 0;
289  (*it).m_lastBlockTime = 0;
290  (*it).m_blockRetryUntil = QDateTime();
291  }
292  else
293  {
294  (*it).m_lastBlockTime =
295  max(m_minBlockSeconds, (*it).m_lastBlockTime * 2);
296  (*it).m_blockRetryUntil =
297  MythDate::current().addSecs((*it).m_lastBlockTime);
298  }
299 
300  QStringList list;
301  list.push_back(QString::number(recordedingID));
302  list.push_back(filename);
303  list.push_back(msg);
304  list.push_back(datetime);
305  QSet<QString>::const_iterator tit = (*it).m_tokens.begin();
306  for (; tit != (*it).m_tokens.end(); ++tit)
307  {
308  kit = m_tokenToKeyMap.find(*tit);
309  if (kit != m_tokenToKeyMap.end())
310  m_tokenToKeyMap.erase(kit);
311  list.push_back(*tit);
312  }
313 
314  if (list.size() > 4)
315  {
316  QSet<QObject*>::iterator sit = m_listeners.begin();
317  for (; sit != m_listeners.end(); ++sit)
318  {
319  MythEvent *le = new MythEvent(me->Message(), list);
320  QCoreApplication::postEvent(*sit, le);
321  }
322  (*it).m_tokens.clear();
323  }
324 
325  m_running = (m_running > 0) ? m_running - 1 : 0;
326  }
327 
329 
330  return true;
331  }
332  return false;
333 }
334 
357  const ProgramInfo &pginfo,
358  const QString &eventname,
359  const QString &filename, const QString &token, const QString &msg,
360  const QDateTime &dt)
361 {
362  QStringList list;
363  list.push_back(QString::number(pginfo.GetRecordingID()));
364  list.push_back(filename);
365  list.push_back(msg);
366  list.push_back(dt.toUTC().toString(Qt::ISODate));
367  list.push_back(token);
368 
369  QMutexLocker locker(&m_lock);
370  QSet<QObject*>::iterator it = m_listeners.begin();
371  for (; it != m_listeners.end(); ++it)
372  {
373  MythEvent *e = new MythEvent(eventname, list);
374  QCoreApplication::postEvent(*it, e);
375  }
376 }
377 
414  ProgramInfo &pginfo,
415  const QSize &size,
416  const QString &outputfile,
417  long long time, bool in_seconds,
418  const QString& token)
419 {
420  QString key = QString("%1_%2x%3_%4%5")
421  .arg(pginfo.GetBasename()).arg(size.width()).arg(size.height())
422  .arg(time).arg(in_seconds?"s":"f");
423 
424  if (pginfo.GetAvailableStatus() == asPendingDelete)
425  {
426  SendEvent(pginfo, "PREVIEW_FAILED", key, token,
427  "Pending Delete", QDateTime());
428  return QString();
429  }
430 
431  // keep in sync with default filename in PreviewGenerator::RunReal
432  QString filename = (outputfile.isEmpty()) ?
433  pginfo.GetPathname() + ".png" : outputfile;
434  QString ret_file = filename;
435  QString ret;
436 
437  bool is_special = !outputfile.isEmpty() || time >= 0 ||
438  size.width() || size.height();
439 
440  bool needs_gen = true;
441  if (!is_special)
442  {
443  QDateTime previewLastModified;
444  bool streaming = !filename.startsWith("/");
445  bool locally_accessible = false;
446  bool bookmark_updated = false;
447 
448  QDateTime bookmark_ts = pginfo.GetBookmarkUpdate();
449  QDateTime cmp_ts;
450  if (bookmark_ts.isValid())
451  cmp_ts = bookmark_ts;
452  else if (MythDate::current() >= pginfo.GetRecordingEndTime())
453  cmp_ts = pginfo.GetLastModifiedTime();
454  else
455  cmp_ts = pginfo.GetRecordingStartTime();
456 
457  if (streaming)
458  {
459  ret_file = QString("%1/%2")
460  .arg(GetRemoteCacheDir()).arg(filename.section('/', -1));
461 
462  QFileInfo finfo(ret_file);
463  if (finfo.isReadable() && finfo.lastModified() >= cmp_ts)
464  {
465  // This is just an optimization to avoid
466  // hitting the backend if our cached copy
467  // is newer than the bookmark, or if we have
468  // a preview and do not update it when the
469  // bookmark changes.
470  previewLastModified = finfo.lastModified();
471  }
472  else if (!IsGeneratingPreview(key))
473  {
474  previewLastModified =
475  RemoteGetPreviewIfModified(pginfo, ret_file);
476  }
477  }
478  else
479  {
480  QFileInfo fi(filename);
481  if ((locally_accessible = fi.isReadable()))
482  previewLastModified = fi.lastModified();
483  }
484 
485  bookmark_updated =
486  (!previewLastModified.isValid() || (previewLastModified <= cmp_ts));
487 
488  if (bookmark_updated && bookmark_ts.isValid() &&
489  previewLastModified.isValid())
490  {
492  }
493 
494  bool preview_exists = previewLastModified.isValid();
495 
496 #if 0
497  QString alttext = (bookmark_ts.isValid()) ? QString() :
498  QString("\n\t\t\tcmp_ts: %1")
499  .arg(cmp_ts.toString(Qt::ISODate));
500  LOG(VB_GENERAL, LOG_INFO,
501  QString("previewLastModified: %1\n\t\t\t"
502  "bookmark_ts: %2%3\n\t\t\t"
503  "pginfo.lastmodified: %4")
504  .arg(previewLastModified.toString(Qt::ISODate))
505  .arg(bookmark_ts.toString(Qt::ISODate))
506  .arg(alttext)
507  .arg(pginfo.GetLastModifiedTime(MythDate::ISODate)) +
508  QString("Title: %1\n\t\t\t")
509  .arg(pginfo.toString(ProgramInfo::kTitleSubtitle)) +
510  QString("File '%1' \n\t\t\tCache '%2'")
511  .arg(filename).arg(ret_file) +
512  QString("\n\t\t\tPreview Exists: %1, Bookmark Updated: %2, "
513  "Need Preview: %3")
514  .arg(preview_exists).arg(bookmark_updated)
515  .arg((bookmark_updated || !preview_exists)));
516 #endif
517 
518  needs_gen = bookmark_updated || !preview_exists;
519 
520  if (!needs_gen)
521  {
522  if (locally_accessible)
523  ret = filename;
524  else if (preview_exists && QFileInfo(ret_file).isReadable())
525  ret = ret_file;
526  }
527  }
528 
529  if (needs_gen && !IsGeneratingPreview(key))
530  {
531  uint attempts = IncPreviewGeneratorAttempts(key);
532  if (attempts < m_maxAttempts)
533  {
534  LOG(VB_PLAYBACK, LOG_INFO, LOC +
535  QString("Requesting preview for '%1'") .arg(key));
536  PreviewGenerator *pg = new PreviewGenerator(&pginfo, token, m_mode);
537  if (!outputfile.isEmpty() || time >= 0 ||
538  size.width() || size.height())
539  {
540  pg->SetPreviewTime(time, in_seconds);
541  pg->SetOutputFilename(outputfile);
542  pg->SetOutputSize(size);
543  }
544 
545  SetPreviewGenerator(key, pg);
546 
547  LOG(VB_PLAYBACK, LOG_INFO, LOC +
548  QString("Requested preview for '%1'").arg(key));
549  }
550  else
551  {
552  LOG(VB_GENERAL, LOG_ERR, LOC +
553  QString("Attempted to generate preview for '%1' "
554  "%2 times; >= max(%3)")
555  .arg(key).arg(attempts).arg(m_maxAttempts));
556  }
557  }
558  else if (needs_gen)
559  {
560  LOG(VB_PLAYBACK, LOG_INFO, LOC +
561  QString("Not requesting preview for %1,"
562  "as it is already being generated")
563  .arg(pginfo.toString(ProgramInfo::kTitleSubtitle)));
564  IncPreviewGeneratorPriority(key, token);
565  }
566 
568 
569  if (!ret.isEmpty())
570  {
571  QString msg = "On Disk";
572  QDateTime dt = QFileInfo(ret).lastModified();
573  SendEvent(pginfo, "PREVIEW_SUCCESS", ret, token, msg, dt);
574  }
575  else
576  {
577  uint queue_depth, token_cnt;
578  GetInfo(key, queue_depth, token_cnt);
579  QString msg = QString("Queue depth %1, our tokens %2")
580  .arg(queue_depth).arg(token_cnt);
581  SendEvent(pginfo, "PREVIEW_QUEUED", ret, token, msg, QDateTime());
582  }
583 
584  return ret;
585 }
586 
600  const QString &key, uint &queue_depth, uint &token_cnt)
601 {
602  QMutexLocker locker(&m_lock);
603  queue_depth = m_queue.size();
604  PreviewMap::iterator pit = m_previewMap.find(key);
605  token_cnt = (pit == m_previewMap.end()) ? 0 : (*pit).m_tokens.size();
606 }
607 
616  const QString &key, const QString& token)
617 {
618  QMutexLocker locker(&m_lock);
619  m_queue.removeAll(key);
620 
621  PreviewMap::iterator pit = m_previewMap.find(key);
622  if (pit == m_previewMap.end())
623  return;
624 
625  if ((*pit).m_gen && !(*pit).m_genStarted)
626  m_queue.push_back(key);
627 
628  if (!token.isEmpty())
629  {
630  m_tokenToKeyMap[token] = key;
631  (*pit).m_tokens.insert(token);
632  }
633 }
634 
640 {
641  QMutexLocker locker(&m_lock);
642  QStringList &q = m_queue;
643  if (!q.empty() && (m_running < m_maxThreads))
644  {
645  QString fn = q.back();
646  q.pop_back();
647  PreviewMap::iterator it = m_previewMap.find(fn);
648  if (it != m_previewMap.end() && (*it).m_gen && !(*it).m_genStarted)
649  {
650  m_running++;
651  (*it).m_gen->start();
652  (*it).m_genStarted = true;
653  }
654  }
655 }
656 
666  const QString &key, PreviewGenerator *g)
667 {
668  if (!g)
669  return;
670 
671  {
672  QMutexLocker locker(&m_lock);
673  m_tokenToKeyMap[g->GetToken()] = key;
674  PreviewGenState &state = m_previewMap[key];
675  if (state.m_gen)
676  {
677  if (g && state.m_gen != g)
678  {
679  if (!g->GetToken().isEmpty())
680  state.m_tokens.insert(g->GetToken());
681  g->deleteLater();
682  g = nullptr;
683  }
684  }
685  else
686  {
687  g->AttachSignals(this);
688  state.m_gen = g;
689  state.m_genStarted = false;
690  if (!g->GetToken().isEmpty())
691  state.m_tokens.insert(g->GetToken());
692  }
693  }
694 
696 }
697 
712 bool PreviewGeneratorQueue::IsGeneratingPreview(const QString &key) const
713 {
714  PreviewMap::const_iterator it;
715  QMutexLocker locker(&m_lock);
716 
717  if ((it = m_previewMap.find(key)) == m_previewMap.end())
718  return false;
719 
720  if ((*it).m_blockRetryUntil.isValid())
721  return MythDate::current() < (*it).m_blockRetryUntil;
722 
723  return (*it).m_gen;
724 }
725 
735 {
736  QMutexLocker locker(&m_lock);
737  return m_previewMap[key].m_attempts++;
738 }
739 
750 {
751  QMutexLocker locker(&m_lock);
752  m_previewMap[key].m_attempts = 0;
753  m_previewMap[key].m_lastBlockTime = 0;
754  m_previewMap[key].m_blockRetryUntil =
755  MythDate::current().addSecs(-60);
756 }
757 
758 
PreviewGeneratorQueue(PreviewGenerator::Mode mode, uint maxAttempts, uint minBlockSeconds)
void start(QThread::Priority=QThread::InheritPriority)
Tell MThread to start running the thread in the near future.
Definition: mthread.cpp:294
PreviewGenerator::Mode m_mode
uint m_minBlockSeconds
How long after a failed preview generation attempt will the code ignore subsequent requests.
QStringList m_queue
The queue of previews to be generated.
This is a wrapper around QThread that does several additional things.
Definition: mthread.h:46
static void GetPreviewImage(const ProgramInfo &pginfo, const QString &token)
Submit a request for the generation of a preview image.
QDateTime GetBookmarkUpdate(void) const
Definition: programinfo.h:467
static Type MythEventMessage
Definition: mythevent.h:66
#define LOC
This class holds all the state information related to a specific preview generator.
void SetOutputFilename(const QString &)
void ToStringList(QStringList &list) const
Serializes ProgramInfo into a QStringList which can be passed over a socket.
bool wait(unsigned long time=ULONG_MAX)
Wait for the MThread to exit, with a maximum timeout.
Definition: mthread.cpp:311
static void TeardownPreviewGeneratorQueue()
Destroy the singleton queue of preview generators.
QString toString(Verbosity v=kLongDescription, const QString &sep=":", const QString &grp="\"") const
unsigned int uint
Definition: compat.h:140
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
void ClearPreviewGeneratorAttempts(const QString &key)
Clears the number of times we have started a PreviewGenerator to create this file.
void GetInfo(const QString &key, uint &queue_depth, uint &token_cnt)
bool m_genStarted
The preview generator for this file is currently running.
This class creates a preview image of a recording.
static void RemoveListener(QObject *)
Stop receiving notifications when a preview event is generated.
Holds information on recordings and videos.
Definition: programinfo.h:66
void SetPreviewGenerator(const QString &key, PreviewGenerator *g)
Sets the PreviewGenerator for a specific file.
uint IncPreviewGeneratorAttempts(const QString &key)
Increments and returns number of times we have started a PreviewGenerator to create this file.
This class is used as a container for messages.
Definition: mythevent.h:16
QMap< QString, QString > m_tokenToKeyMap
A mapping from requestor tokens to internal keys.
QDateTime RemoteGetPreviewIfModified(const ProgramInfo &pginfo, const QString &cachefile)
Download preview & get timestamp if newer than cachefile's last modified time, otherwise just get the...
Definition: remoteutil.cpp:239
PreviewGenerator * m_gen
A pointer to the generator that this state object describes.
QString GetBasename(void) const
Definition: programinfo.h:336
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:10
void exit(int retcode=0)
Use this to exit from the thread if you are using a Qt event loop.
Definition: mthread.cpp:289
AvailableStatusType GetAvailableStatus(void) const
Definition: programinfo.h:810
QSet< QObject * > m_listeners
The set of all listeners that want messages when a preview request is queued or finishes.
uint GetRecordingID(void) const
Definition: programinfo.h:438
void SetOutputSize(const QSize &size)
int GetNumSetting(const QString &key, int defaultval=0)
#define LOG(_MASK_, _LEVEL_, _STRING_)
Definition: mythlogging.h:41
QMutex m_lock
The thread interlock for this data structure.
static void AddListener(QObject *)
Request notifications when a preview event is generated.
void SendEvent(const ProgramInfo &pginfo, const QString &eventname, const QString &filename, const QString &token, const QString &msg, const QDateTime &dt)
Send a message back to all objects that have requested creation of a specific preview.
const QString & ExtraData(int idx=0) const
Definition: mythevent.h:59
PreviewMap m_previewMap
A mapping from the generated preview name to the state information on the progress of generating the ...
QString GeneratePreviewImage(ProgramInfo &pginfo, const QSize &, const QString &outputfile, long long time, bool in_seconds, const QString &token)
Generate a preview image for the specified program.
uint m_running
The number of threads currently generating previews.
static PreviewGeneratorQueue * s_pgq
The singleton queue.
bool event(QEvent *e) override
The event handler running on the preview generation thread.
QThread * qthread(void)
Returns the thread, this will always return the same pointer no matter how often you restart the thre...
Definition: mthread.cpp:244
QDateTime GetRecordingStartTime(void) const
Approximate time the recording started.
Definition: programinfo.h:396
QDateTime GetRecordingEndTime(void) const
Approximate time the recording should have ended, did end, or is intended to end.
Definition: programinfo.h:404
void SetPreviewTime(long long time, bool in_seconds)
QDateTime GetLastModifiedTime(void) const
Definition: programinfo.h:421
~PreviewGeneratorQueue()
Destroy the preview generation queue.
bool IsGeneratingPreview(const QString &key) const
Is a preview currently being generated for this key.
void UpdatePreviewGeneratorThreads(void)
As long as there are items in the queue, make sure we're running the maximum allowed number of previe...
const QString & Message() const
Definition: mythevent.h:58
uint m_maxAttempts
How many times total will the code attempt to generate a preview for a specific file,...
uint m_maxThreads
The maximum number of threads that may concurrently generate previews.
QString GetPathname(void) const
Definition: programinfo.h:335
QString GetRemoteCacheDir(void)
Returns the directory for all files cached from the backend.
Definition: mythdirs.cpp:241
QSet< QString > m_tokens
The full set of tokens for all callers that have requested this preview.
const QStringList & ExtraDataList() const
Definition: mythevent.h:60
Default UTC.
Definition: mythdate.h:14
void IncPreviewGeneratorPriority(const QString &key, const QString &token)
This class implements a queue of preview generation requests.
static void CreatePreviewGeneratorQueue(PreviewGenerator::Mode mode, uint maxAttempts, uint minBlockSeconds)
Create the singleton queue of preview generators.
unsigned char g
Definition: ParseText.cpp:329