MythTV master
previewgeneratorqueue.cpp
Go to the documentation of this file.
1
3
4// C++
5#include <algorithm>
6
7// QT
8#include <QCoreApplication>
9#include <QFileInfo>
10
11// libmythbase
12#include "libmythbase/mthread.h"
16
17// libmythtv
18#include "previewgenerator.h"
20
21#define LOC QString("PreviewQueue: ")
22
24
41 uint maxAttempts, std::chrono::seconds minBlockSeconds)
42{
43 s_pgq = new PreviewGeneratorQueue(mode, maxAttempts, minBlockSeconds);
44}
45
53{
54 s_pgq->exit(0);
55 s_pgq->wait();
56 delete s_pgq;
57 s_pgq = nullptr;
58}
59
60/*
61 * Create the queue object for holding preview generators.
62 *
63 * Create the singleton queue of preview generators. This should be
64 * called once at program start-up. All generation requests on this
65 * queue will will be created with the maxAttempts and minBlockSeconds
66 * parameters supplied here.
67 *
68 * \param[in] mode Local or Remote (or both)
69 * \param[in] maxAttempts How many times total will the code attempt
70 * to generate a preview for a specific file, before giving
71 * up and ignoring all future requests.
72 * \param[in] minBlockSeconds How long after a failed preview
73 * generation attempt will the code ignore subsequent
74 * requests.
75 *
76 * \note Never call this routine directly. Call the
77 * CreatePreviewGeneratorQueue function instead.
78 */
81 uint maxAttempts, std::chrono::seconds minBlockSeconds) :
82 MThread("PreviewGeneratorQueue"),
83 m_mode(mode),
84 m_maxAttempts(maxAttempts), m_minBlockSeconds(minBlockSeconds)
85{
86 if (PreviewGenerator::kLocal & mode)
87 {
88 int idealThreads = QThread::idealThreadCount();
89 m_maxThreads = (idealThreads >= 1) ? idealThreads * 2 : 2;
90 }
91
92 moveToThread(qthread());
93 start();
94}
95
105{
106 // disconnect preview generators
107 QMutexLocker locker(&m_lock);
108 // NOLINTNEXTLINE(modernize-loop-convert)
109 for (auto it = m_previewMap.begin(); it != m_previewMap.end(); ++it)
110 {
111 if ((*it).m_gen)
112 (*it).m_gen->deleteLater();
113 (*it).m_gen = nullptr;
114 }
115 locker.unlock();
116 wait();
117}
118
141 const ProgramInfo &pginfo,
142 const QSize outputsize,
143 const QString &outputfile,
144 std::chrono::seconds time, long long frame,
145 const QString& token)
146{
147 if (!s_pgq)
148 return;
149
150 if (pginfo.GetPathname().isEmpty() ||
151 pginfo.GetBasename() == pginfo.GetPathname())
152 {
153 return;
154 }
155
156 if (gCoreContext->GetNumSetting("JobAllowPreview", 1) == 0)
157 return;
158
159 QStringList extra;
160 pginfo.ToStringList(extra);
161 extra += token;
162 extra += QString::number(outputsize.width());
163 extra += QString::number(outputsize.height());
164 extra += outputfile;
165 if (time >= 0s)
166 {
167 extra += QString::number(time.count());
168 extra += "1";
169 }
170 else
171 {
172 extra += QString::number(frame);
173 extra += "0";
174 }
175 auto *e = new MythEvent("GET_PREVIEW", extra);
176 QCoreApplication::postEvent(s_pgq, e);
177}
178
187{
188 if (!s_pgq)
189 return;
190
191 QMutexLocker locker(&s_pgq->m_lock);
192 s_pgq->m_listeners.insert(listener);
193}
194
203{
204 if (!s_pgq)
205 return;
206
207 QMutexLocker locker(&s_pgq->m_lock);
208 s_pgq->m_listeners.remove(listener);
209}
210
228{
229 if (e->type() != MythEvent::kMythEventMessage)
230 return QObject::event(e);
231
232 auto *me = dynamic_cast<MythEvent*>(e);
233 if (me == nullptr)
234 return QObject::event(e);
235 if (me->Message() == "GET_PREVIEW")
236 {
237 const QStringList &list = me->ExtraDataList();
238 QStringList::const_iterator it = list.begin();
239 ProgramInfo evinfo(it, list.end());
240 QString token;
241 QSize outputsize;
242 QString outputfile;
243 long long time_or_frame = -1LL;
244 if (it != list.end())
245 token = (*it++);
246 if (it != list.end())
247 outputsize.setWidth((*it++).toInt());
248 if (it != list.end())
249 outputsize.setHeight((*it++).toInt());
250 if (it != list.end())
251 outputfile = (*it++);
252 if (it != list.end())
253 time_or_frame = (*it++).toLongLong();
254 if (it != list.end())
255 {
256 bool time_fmt_sec = (*it++).toInt() != 0;
257 if (time_fmt_sec)
258 {
259 GeneratePreviewImage(evinfo, outputsize, outputfile,
260 std::chrono::seconds(time_or_frame), -1, token);
261 }
262 else
263 {
264 GeneratePreviewImage(evinfo, outputsize, outputfile,
265 -1s, time_or_frame, token);
266 }
267 }
268 return true;
269 }
270 if (me->Message() == "PREVIEW_SUCCESS" ||
271 me->Message() == "PREVIEW_FAILED")
272 {
273 uint recordedingID = me->ExtraData(0).toUInt(); // pginfo->GetRecordingID()
274 const QString& filename = me->ExtraData(1); // outFileName
275 const QString& msg = me->ExtraData(2);
276 const QString& datetime = me->ExtraData(3);
277 const QString& token = me->ExtraData(4);
278
279 {
280 QMutexLocker locker(&m_lock);
281 QMap<QString,QString>::iterator kit = m_tokenToKeyMap.find(token);
282 if (kit == m_tokenToKeyMap.end())
283 {
284 LOG(VB_GENERAL, LOG_ERR, LOC +
285 QString("Failed to find token %1 in map.").arg(token));
286 return true;
287 }
288 PreviewMap::iterator it = m_previewMap.find(*kit);
289 if (it == m_previewMap.end())
290 {
291 LOG(VB_GENERAL, LOG_ERR, LOC +
292 QString("Failed to find key %1 in map.").arg(*kit));
293 return true;
294 }
295
296 if ((*it).m_gen)
297 (*it).m_gen->deleteLater();
298 (*it).m_gen = nullptr;
299 (*it).m_genStarted = false;
300 if (me->Message() == "PREVIEW_SUCCESS")
301 {
302 (*it).m_attempts = 0;
303 (*it).m_lastBlockTime = 0s;
304 (*it).m_blockRetryUntil = QDateTime();
305 }
306 else
307 {
308 (*it).m_lastBlockTime =
309 std::max(m_minBlockSeconds, (*it).m_lastBlockTime * 2);
310 (*it).m_blockRetryUntil =
311 MythDate::current().addSecs((*it).m_lastBlockTime.count());
312 }
313
314 QStringList list;
315 list.reserve(4 + (*it).m_tokens.size());
316 list.push_back(QString::number(recordedingID));
317 list.push_back(filename);
318 list.push_back(msg);
319 list.push_back(datetime);
320 for (const auto & tok : std::as_const((*it).m_tokens))
321 {
322 kit = m_tokenToKeyMap.find(tok);
323 if (kit != m_tokenToKeyMap.end())
324 m_tokenToKeyMap.erase(kit);
325 list.push_back(tok);
326 }
327
328 if (list.size() > 4)
329 {
330 for (auto *listener : std::as_const(m_listeners))
331 {
332 auto *le = new MythEvent(me->Message(), list);
333 QCoreApplication::postEvent(listener, le);
334 }
335 (*it).m_tokens.clear();
336 }
337
338 m_running = (m_running > 0) ? m_running - 1 : 0;
339 }
340
342
343 return true;
344 }
345 return QObject::event(e);
346}
347
370 const ProgramInfo &pginfo,
371 const QString &eventname,
372 const QString &filename, const QString &token, const QString &msg,
373 const QDateTime &dt)
374{
375 QStringList list;
376 list.push_back(QString::number(pginfo.GetRecordingID()));
377 list.push_back(filename);
378 list.push_back(msg);
379 list.push_back(dt.toUTC().toString(Qt::ISODate));
380 list.push_back(token);
381
382 QMutexLocker locker(&m_lock);
383 for (auto *listener : std::as_const(m_listeners))
384 {
385 auto *e = new MythEvent(eventname, list);
386 QCoreApplication::postEvent(listener, e);
387 }
388}
389
426 ProgramInfo &pginfo,
427 const QSize size,
428 const QString &outputfile,
429 std::chrono::seconds time, long long frame,
430 const QString& token)
431{
432 auto pos_text = (time >= 0s)
433 ? QString::number(time.count()) + "s"
434 : QString::number(frame)+ "f";
435 QString key = QString("%1_%2x%3_%4")
436 .arg(pginfo.GetBasename()).arg(size.width()).arg(size.height())
437 .arg(pos_text);
438
439 if (pginfo.GetAvailableStatus() == asPendingDelete)
440 {
441 SendEvent(pginfo, "PREVIEW_FAILED", key, token,
442 "Pending Delete", QDateTime());
443 return {};
444 }
445
446 // keep in sync with default filename in PreviewGenerator::RunReal
447 QString filename = (outputfile.isEmpty()) ?
448 pginfo.GetPathname() + ".png" : outputfile;
449 QString ret_file = filename;
450 QString ret;
451
452 bool is_special = !outputfile.isEmpty() || time >= 0s ||
453 (size.width() != 0) || (size.height() != 0);
454
455 bool needs_gen = true;
456 if (!is_special)
457 {
458 QDateTime previewLastModified;
459 bool streaming = !filename.startsWith("/");
460 bool locally_accessible = false;
461 bool bookmark_updated = false;
462
463 QDateTime bookmark_ts = pginfo.GetBookmarkUpdate();
464 QDateTime cmp_ts;
465 if (bookmark_ts.isValid())
466 cmp_ts = bookmark_ts;
467 else if (MythDate::current() >= pginfo.GetRecordingEndTime())
468 cmp_ts = pginfo.GetLastModifiedTime();
469 else
470 cmp_ts = pginfo.GetRecordingStartTime();
471
472 if (streaming)
473 {
474 ret_file = QString("%1/%2")
475 .arg(GetRemoteCacheDir(), filename.section('/', -1));
476
477 QFileInfo finfo(ret_file);
478 if (finfo.isReadable() && finfo.lastModified() >= cmp_ts)
479 {
480 // This is just an optimization to avoid
481 // hitting the backend if our cached copy
482 // is newer than the bookmark, or if we have
483 // a preview and do not update it when the
484 // bookmark changes.
485 previewLastModified = finfo.lastModified();
486 }
487 else if (!IsGeneratingPreview(key))
488 {
489 previewLastModified =
490 RemoteGetPreviewIfModified(pginfo, ret_file);
491 }
492 }
493 else
494 {
495 QFileInfo fi(filename);
496 locally_accessible = fi.isReadable();
497 if (locally_accessible)
498 previewLastModified = fi.lastModified();
499 }
500
501 bookmark_updated =
502 (!previewLastModified.isValid() || (previewLastModified <= cmp_ts));
503
504 if (bookmark_updated && bookmark_ts.isValid() &&
505 previewLastModified.isValid())
506 {
508 }
509
510 bool preview_exists = previewLastModified.isValid();
511
512#if 0
513 QString alttext = (bookmark_ts.isValid()) ? QString() :
514 QString("\n\t\t\tcmp_ts: %1")
515 .arg(cmp_ts.toString(Qt::ISODate));
516 LOG(VB_GENERAL, LOG_INFO,
517 QString("previewLastModified: %1\n\t\t\t"
518 "bookmark_ts: %2%3\n\t\t\t"
519 "pginfo.lastmodified: %4")
520 .arg(previewLastModified.toString(Qt::ISODate))
521 .arg(bookmark_ts.toString(Qt::ISODate))
522 .arg(alttext)
524 QString("Title: %1\n\t\t\t")
526 QString("File '%1' \n\t\t\tCache '%2'")
527 .arg(filename).arg(ret_file) +
528 QString("\n\t\t\tPreview Exists: %1, Bookmark Updated: %2, "
529 "Need Preview: %3")
530 .arg(preview_exists).arg(bookmark_updated)
531 .arg((bookmark_updated || !preview_exists)));
532#endif
533
534 needs_gen = bookmark_updated || !preview_exists;
535
536 if (!needs_gen)
537 {
538 if (locally_accessible)
539 ret = filename;
540 else if (preview_exists && QFileInfo(ret_file).isReadable())
541 ret = ret_file;
542 }
543 }
544
545 if (needs_gen && !IsGeneratingPreview(key))
546 {
547 uint attempts = IncPreviewGeneratorAttempts(key);
548 if (attempts < m_maxAttempts)
549 {
550 LOG(VB_PLAYBACK, LOG_INFO, LOC +
551 QString("Requesting preview for '%1'") .arg(key));
552 auto *pg = new PreviewGenerator(&pginfo, token, m_mode);
553 if (!outputfile.isEmpty() || time >= 0s ||
554 size.width() || size.height())
555 {
556 pg->SetPreviewTime(time, frame);
557 pg->SetOutputFilename(outputfile);
558 pg->SetOutputSize(size);
559 }
560
561 SetPreviewGenerator(key, pg);
562
563 LOG(VB_PLAYBACK, LOG_INFO, LOC +
564 QString("Requested preview for '%1'").arg(key));
565 }
566 else
567 {
568 LOG(VB_GENERAL, LOG_ERR, LOC +
569 QString("Attempted to generate preview for '%1' "
570 "%2 times; >= max(%3)")
571 .arg(key).arg(attempts).arg(m_maxAttempts));
572 }
573 }
574 else if (needs_gen)
575 {
576 LOG(VB_PLAYBACK, LOG_INFO, LOC +
577 QString("Not requesting preview for %1,"
578 "as it is already being generated")
580 IncPreviewGeneratorPriority(key, token);
581 }
582
584
585 if (!ret.isEmpty())
586 {
587 QString msg = "On Disk";
588 QDateTime dt = QFileInfo(ret).lastModified();
589 SendEvent(pginfo, "PREVIEW_SUCCESS", ret, token, msg, dt);
590 }
591 else
592 {
593 uint queue_depth = 0;
594 uint token_cnt = 0;
595 GetInfo(key, queue_depth, token_cnt);
596 QString msg = QString("Queue depth %1, our tokens %2")
597 .arg(queue_depth).arg(token_cnt);
598 SendEvent(pginfo, "PREVIEW_QUEUED", ret, token, msg, QDateTime());
599 }
600
601 return ret;
602}
603
617 const QString &key, uint &queue_depth, uint &token_cnt)
618{
619 QMutexLocker locker(&m_lock);
620 queue_depth = m_queue.size();
621 PreviewMap::iterator pit = m_previewMap.find(key);
622 token_cnt = (pit == m_previewMap.end()) ? 0 : (*pit).m_tokens.size();
623}
624
633 const QString &key, const QString& token)
634{
635 QMutexLocker locker(&m_lock);
636 m_queue.removeAll(key);
637
638 PreviewMap::iterator pit = m_previewMap.find(key);
639 if (pit == m_previewMap.end())
640 return;
641
642 if ((*pit).m_gen && !(*pit).m_genStarted)
643 m_queue.push_back(key);
644
645 if (!token.isEmpty())
646 {
647 m_tokenToKeyMap[token] = key;
648 (*pit).m_tokens.insert(token);
649 }
650}
651
657{
658 QMutexLocker locker(&m_lock);
659 QStringList &q = m_queue;
660 if (!q.empty() && (m_running < m_maxThreads))
661 {
662 QString fn = q.back();
663 q.pop_back();
664 PreviewMap::iterator it = m_previewMap.find(fn);
665 if (it != m_previewMap.end() && (*it).m_gen && !(*it).m_genStarted)
666 {
667 m_running++;
668 (*it).m_gen->start();
669 (*it).m_genStarted = true;
670 }
671 }
672}
673
683 const QString &key, PreviewGenerator *g)
684{
685 if (!g)
686 return;
687
688 {
689 QMutexLocker locker(&m_lock);
690 m_tokenToKeyMap[g->GetToken()] = key;
691 PreviewGenState &state = m_previewMap[key];
692 if (state.m_gen)
693 {
694 if (state.m_gen != g)
695 {
696 if (!g->GetToken().isEmpty())
697 state.m_tokens.insert(g->GetToken());
698 g->deleteLater();
699 g = nullptr;
700 }
701 }
702 else
703 {
704 g->AttachSignals(this);
705 state.m_gen = g;
706 state.m_genStarted = false;
707 if (!g->GetToken().isEmpty())
708 state.m_tokens.insert(g->GetToken());
709 }
710 }
711
713}
714
729bool PreviewGeneratorQueue::IsGeneratingPreview(const QString &key) const
730{
731 QMutexLocker locker(&m_lock);
732
733 PreviewMap::const_iterator it = m_previewMap.find(key);
734 if (it == m_previewMap.end())
735 return false;
736
737 if ((*it).m_blockRetryUntil.isValid())
738 return MythDate::current() < (*it).m_blockRetryUntil;
739
740 return (*it).m_gen;
741}
742
752{
753 QMutexLocker locker(&m_lock);
754 return m_previewMap[key].m_attempts++;
755}
756
767{
768 QMutexLocker locker(&m_lock);
769 m_previewMap[key].m_attempts = 0;
770 m_previewMap[key].m_lastBlockTime = 0s;
771 m_previewMap[key].m_blockRetryUntil =
772 MythDate::current().addSecs(-60);
773}
774
775
793#include "moc_previewgeneratorqueue.cpp"
This is a wrapper around QThread that does several additional things.
Definition: mthread.h:49
void start(QThread::Priority p=QThread::InheritPriority)
Tell MThread to start running the thread in the near future.
Definition: mthread.cpp:267
bool wait(std::chrono::milliseconds time=std::chrono::milliseconds::max())
Wait for the MThread to exit, with a maximum timeout.
Definition: mthread.cpp:284
void exit(int retcode=0)
Use this to exit from the thread if you are using a Qt event loop.
Definition: mthread.cpp:262
QThread * qthread(void)
Returns the thread, this will always return the same pointer no matter how often you restart the thre...
Definition: mthread.cpp:217
int GetNumSetting(const QString &key, int defaultval=0)
This class is used as a container for messages.
Definition: mythevent.h:17
static const Type kMythEventMessage
Definition: mythevent.h:79
This class holds all the state information related to a specific preview generator.
bool m_genStarted
The preview generator for this file is currently running.
QSet< QString > m_tokens
The full set of tokens for all callers that have requested this preview.
PreviewGenerator * m_gen
A pointer to the generator that this state object describes.
This class implements a queue of preview generation requests.
uint m_maxAttempts
How many times total will the code attempt to generate a preview for a specific file,...
QMap< QString, QString > m_tokenToKeyMap
A mapping from requestor tokens to internal keys.
QSet< QObject * > m_listeners
The set of all listeners that want messages when a preview request is queued or finishes.
std::chrono::seconds m_minBlockSeconds
How long after a failed preview generation attempt will the code ignore subsequent requests.
static PreviewGeneratorQueue * s_pgq
The singleton queue.
bool IsGeneratingPreview(const QString &key) const
Is a preview currently being generated for this key.
void SetPreviewGenerator(const QString &key, PreviewGenerator *g)
Sets the PreviewGenerator for a specific file.
~PreviewGeneratorQueue() override
Destroy the preview generation queue.
uint IncPreviewGeneratorAttempts(const QString &key)
Increments and returns number of times we have started a PreviewGenerator to create this file.
QString GeneratePreviewImage(ProgramInfo &pginfo, QSize size, const QString &outputfile, std::chrono::seconds time, long long frame, const QString &token)
Generate a preview image for the specified program.
void IncPreviewGeneratorPriority(const QString &key, const QString &token)
QStringList m_queue
The queue of previews to be generated.
void UpdatePreviewGeneratorThreads(void)
As long as there are items in the queue, make sure we're running the maximum allowed number of previe...
PreviewGenerator::Mode m_mode
void GetInfo(const QString &key, uint &queue_depth, uint &token_cnt)
static void GetPreviewImage(const ProgramInfo &pginfo, const QString &token)
Submit a request for the generation of a preview image.
uint m_maxThreads
The maximum number of threads that may concurrently generate previews.
PreviewGeneratorQueue(PreviewGenerator::Mode mode, uint maxAttempts, std::chrono::seconds minBlockSeconds)
void ClearPreviewGeneratorAttempts(const QString &key)
Clears the number of times we have started a PreviewGenerator to create this file.
static void CreatePreviewGeneratorQueue(PreviewGenerator::Mode mode, uint maxAttempts, std::chrono::seconds minBlockSeconds)
Create the singleton queue of preview generators.
static void AddListener(QObject *listener)
Request notifications when a preview event is generated.
static void RemoveListener(QObject *listener)
Stop receiving notifications when a preview event is generated.
PreviewMap m_previewMap
A mapping from the generated preview name to the state information on the progress of generating the ...
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.
uint m_running
The number of threads currently generating previews.
QMutex m_lock
The thread interlock for this data structure.
static void TeardownPreviewGeneratorQueue()
Destroy the singleton queue of preview generators.
bool event(QEvent *e) override
The event handler running on the preview generation thread.
This class creates a preview image of a recording.
QString GetToken(void) const
void AttachSignals(QObject *obj)
Holds information on recordings and videos.
Definition: programinfo.h:75
QString GetBasename(void) const
Definition: programinfo.h:352
QString toString(Verbosity v=kLongDescription, const QString &sep=":", const QString &grp="\"") const
QDateTime GetBookmarkUpdate(void) const
Definition: programinfo.h:486
uint GetRecordingID(void) const
Definition: programinfo.h:458
AvailableStatusType GetAvailableStatus(void) const
Definition: programinfo.h:856
QDateTime GetLastModifiedTime(void) const
Definition: programinfo.h:441
QDateTime GetRecordingStartTime(void) const
Approximate time the recording started.
Definition: programinfo.h:413
QString GetPathname(void) const
Definition: programinfo.h:351
void ToStringList(QStringList &list) const
Serializes ProgramInfo into a QStringList which can be passed over a socket.
QDateTime GetRecordingEndTime(void) const
Approximate time the recording should have ended, did end, or is intended to end.
Definition: programinfo.h:421
unsigned int uint
Definition: compat.h:60
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
QString GetRemoteCacheDir(void)
Returns the directory for all files cached from the backend.
Definition: mythdirs.cpp:298
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
@ ISODate
Default UTC.
Definition: mythdate.h:18
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:15
#define LOC
QDateTime RemoteGetPreviewIfModified(const ProgramInfo &pginfo, const QString &cachefile)
Download preview & get timestamp if newer than cachefile's last modified time, otherwise just get the...
@ asPendingDelete
Definition: programtypes.h:178