MythTV  master
tv_play.cpp
Go to the documentation of this file.
1 // Std
2 #include <algorithm>
3 #include <chrono>
4 #include <cmath>
5 #include <cstdarg>
6 #include <cstdint>
7 #include <cstdlib>
8 #include <thread>
9 
10 // Qt
11 #include <QApplication>
12 #include <QDomDocument>
13 #include <QDomElement>
14 #include <QDomNode>
15 #include <QEvent>
16 #include <QFile>
17 #include <QKeyEvent>
18 #include <QRegularExpression>
19 #include <QRunnable>
20 #include <QTimerEvent>
21 #include <utility>
22 
23 #include "libmythbase/mythconfig.h"
24 
25 // libmythbase
26 #include "libmythbase/compat.h"
27 #include "libmythbase/lcddevice.h"
31 #include "libmythbase/mythdate.h"
32 #include "libmythbase/mythdb.h"
33 #include "libmythbase/mythdirs.h"
35 #include "libmythbase/mythmedia.h"
38 
39 // libmyth
40 #include "libmyth/programinfo.h"
41 #include "libmyth/programtypes.h"
42 #include "libmyth/remoteutil.h"
43 
44 // libmythui
50 #include "libmythui/mythuihelper.h"
52 
53 // libmythtv
54 #include "Bluray/mythbdbuffer.h"
55 #include "Bluray/mythbdplayer.h"
56 #include "DVD/mythdvdbuffer.h"
57 #include "DVD/mythdvdplayer.h"
58 #include "cardutil.h"
59 #include "channelutil.h"
61 #include "io/mythmediabuffer.h"
62 #include "jobqueue.h"
63 #include "livetvchain.h"
64 #include "mythplayerui.h"
65 #include "mythsystemevent.h"
66 #include "mythtvactionutils.h"
67 #include "playercontext.h"
68 #include "playgroup.h"
69 #include "recordinginfo.h"
70 #include "recordingrule.h"
71 #include "remoteencoder.h"
72 #include "signalmonitorvalue.h"
73 #include "sourceutil.h"
74 #include "tv_play.h"
75 #include "tv_play_win.h"
76 #include "tvremoteutil.h"
77 #include "videometadatautil.h"
78 
79 #define DEBUG_CHANNEL_PREFIX 0
80 #define DEBUG_ACTIONS 0
82 #define LOC QString("TV::%1(): ").arg(__func__)
83 
84 #define HideOSDWindow(WINDOW) { \
85  OSD *osd = GetOSDL(); \
86  if (osd) \
87  osd->HideWindow(WINDOW); \
88  ReturnOSDLock(); }
89 
94 {
95  int count = 0;
96 
98  query.prepare("SELECT COUNT(cardid) FROM capturecard;");
99  if (query.exec() && query.isActive() && query.size() && query.next())
100  count = query.value(0).toInt();
101 
102  LOG(VB_RECORD, LOG_INFO,
103  "ConfiguredTunerCards() = " + QString::number(count));
104 
105  return count;
106 }
107 
115 TV* TV::AcquireRelease(int& RefCount, bool Acquire, bool Create /*=false*/)
116 {
117  static QMutex s_lock;
118  static TV* s_tv = nullptr;
119  QMutexLocker locker(&s_lock);
120 
121  if (Acquire)
122  {
123  if (!s_tv && Create)
124  s_tv = new TV(GetMythMainWindow());
125  else if (s_tv)
126  s_tv->IncrRef();
127  }
128  else
129  {
130  if (!s_tv)
131  LOG(VB_GENERAL, LOG_ERR, LOC + "Ref count error");
132  else
133  if (s_tv->DecrRef() == 0)
134  s_tv = nullptr;
135  }
136 
137  if (s_tv)
138  RefCount = s_tv->m_referenceCount;
139  else
140  RefCount = 0;
141  return s_tv;
142 }
143 
149 {
150  bool result = false;
151  int dummy = 0;
152  TV* tv = AcquireRelease(dummy, true);
153  if (tv)
154  {
155  result = true;
156  AcquireRelease(dummy, false);
157  }
158  return result;
159 }
160 
169 {
170  return &m_playerContext;
171 }
172 
173 bool TV::CreatePlayer(TVState State, bool Muted)
174 {
176  {
177  LOG(VB_GENERAL, LOG_ERR, LOC + "Already have a player");
178  return false;
179  }
180 
181  uint playerflags = kDecodeAllowGPU;
182  playerflags |= Muted ? kAudioMuted : kNoFlags;
183  auto flags = static_cast<PlayerFlags>(playerflags);
184 
185  MythPlayerUI *player = nullptr;
186  if (kState_WatchingBD == State)
187  player = new MythBDPlayer(m_mainWindow, this, &m_playerContext, flags);
188  else if (kState_WatchingDVD == State)
189  player = new MythDVDPlayer(m_mainWindow, this, &m_playerContext, flags);
190  else
191  player = new MythPlayerUI(m_mainWindow, this, &m_playerContext, flags);
192 
194 
195  bool isWatchingRecording = (State == kState_WatchingRecording);
196  player->SetWatchingRecording(isWatchingRecording);
197 
198  m_playerContext.SetPlayer(player);
199  emit InitialisePlayerState();
200  m_player = player;
201  return StartPlaying(-1ms);
202 }
203 
209 bool TV::StartPlaying(std::chrono::milliseconds MaxWait)
210 {
211  if (!m_player)
212  return false;
213 
214  if (!m_player->StartPlaying())
215  {
216  LOG(VB_GENERAL, LOG_ERR, LOC + "StartPlaying() Failed to start player");
217  // no need to call StopPlaying here as the player context will be deleted
218  // later following the error
219  return false;
220  }
221  MaxWait = (MaxWait <= 0ms) ? 20s : MaxWait;
222 #ifdef USING_VALGRIND
223  MaxWait = std::chrono::milliseconds::max();
224 #endif // USING_VALGRIND
225  MythTimer t;
226  t.start();
227 
228  while (!m_player->IsPlaying(50ms, true) && (t.elapsed() < MaxWait))
230 
231  if (m_player->IsPlaying())
232  {
233  LOG(VB_PLAYBACK, LOG_INFO, LOC +
234  QString("StartPlaying(): took %1 ms to start player.")
235  .arg(t.elapsed().count()));
236  return true;
237  }
238  LOG(VB_GENERAL, LOG_ERR, LOC + "StartPlaying() Failed to start player");
240  return false;
241 }
242 
244 {
246  PrepareToExitPlayer(__LINE__);
247  SetExitPlayer(true, true);
250 }
251 
262 bool TV::StartTV(ProgramInfo* TVRec, uint Flags, const ChannelInfoList& Selection)
263 {
264  int refs = 0;
265  TV* tv = AcquireRelease(refs, true, true);
266  // handle existing TV object atomically
267  if (refs > 1)
268  {
269  AcquireRelease(refs, false);
270  LOG(VB_GENERAL, LOG_WARNING, LOC + "Already have a TV object.");
272  return false;
273  }
274 
275  LOG(VB_PLAYBACK, LOG_DEBUG, LOC + "-- begin");
276  bool inPlaylist = (Flags & kStartTVInPlayList) != 0U;
277  bool initByNetworkCommand = (Flags & kStartTVByNetworkCommand) != 0U;
278  bool quitAll = false;
279  bool showDialogs = true;
280  bool playCompleted = false;
281  ProgramInfo *curProgram = nullptr;
282  bool startSysEventSent = false;
283  bool startLivetvEventSent = false;
284 
285  if (TVRec)
286  {
287  curProgram = new ProgramInfo(*TVRec);
288  curProgram->SetIgnoreBookmark((Flags & kStartTVIgnoreBookmark) != 0U);
289  curProgram->SetIgnoreProgStart((Flags & kStartTVIgnoreProgStart) != 0U);
290  curProgram->SetIgnoreLastPlayPos((Flags & kStartTVIgnoreLastPlayPos) != 0U);
291  }
292 
293  // Initialize TV
294  if (!tv->Init())
295  {
296  LOG(VB_GENERAL, LOG_ERR, LOC + "Failed initializing TV");
297  AcquireRelease(refs, false);
298  delete curProgram;
300  return false;
301  }
302 
303  if (!lastProgramStringList.empty())
304  {
306  if (pginfo.HasPathname() || pginfo.GetChanID())
307  tv->SetLastProgram(&pginfo);
308  }
309 
310  // Notify others that we are about to play
312 
313  QString playerError;
314  while (!quitAll)
315  {
316  if (curProgram)
317  {
318  LOG(VB_PLAYBACK, LOG_DEBUG, LOC + "tv->Playback() -- begin");
319  if (!tv->Playback(*curProgram))
320  {
321  quitAll = true;
322  }
323  else if (!startSysEventSent)
324  {
325  startSysEventSent = true;
326  SendMythSystemPlayEvent("PLAY_STARTED", curProgram);
327  }
328 
329  LOG(VB_PLAYBACK, LOG_DEBUG, LOC + "tv->Playback() -- end");
330  }
331  else if (RemoteGetFreeRecorderCount())
332  {
333  LOG(VB_PLAYBACK, LOG_DEBUG, LOC + "tv->LiveTV() -- begin");
334  if (!tv->LiveTV(showDialogs, Selection))
335  {
336  tv->SetExitPlayer(true, true);
337  quitAll = true;
338  }
339  else if (!startSysEventSent)
340  {
341  startSysEventSent = true;
342  startLivetvEventSent = true;
343  gCoreContext->SendSystemEvent("LIVETV_STARTED");
344  }
345 
346  LOG(VB_PLAYBACK, LOG_DEBUG, LOC + "tv->LiveTV() -- end");
347  }
348  else
349  {
350  if (!ConfiguredTunerCards())
351  LOG(VB_GENERAL, LOG_ERR, LOC + "No tuners configured");
352  else
353  LOG(VB_GENERAL, LOG_ERR, LOC + "No tuners free for live tv");
354  quitAll = true;
355  continue;
356  }
357 
358  tv->SetInPlayList(inPlaylist);
359  tv->setUnderNetworkControl(initByNetworkCommand);
360 
362 
363  // Process Events
364  LOG(VB_GENERAL, LOG_INFO, LOC + "Entering main playback loop.");
365  tv->PlaybackLoop();
366  LOG(VB_GENERAL, LOG_INFO, LOC + "Exiting main playback loop.");
367 
368  if (tv->GetJumpToProgram())
369  {
370  ProgramInfo *nextProgram = tv->GetLastProgram();
371 
372  tv->SetLastProgram(curProgram);
373  delete curProgram;
374  curProgram = nextProgram;
375 
376  SendMythSystemPlayEvent("PLAY_CHANGED", curProgram);
377  continue;
378  }
379 
380  tv->GetPlayerReadLock();
381  PlayerContext* context = tv->GetPlayerContext();
382  quitAll = tv->m_wantsToQuit || (context->m_errored);
383  context->LockDeletePlayer(__FILE__, __LINE__);
384  if (context->m_player && context->m_player->IsErrored())
385  playerError = context->m_player->GetError();
386  context->UnlockDeletePlayer(__FILE__, __LINE__);
387  tv->ReturnPlayerLock();
388  quitAll |= !playerError.isEmpty();
389  }
390 
391  QCoreApplication::processEvents();
392 
393  // check if the show has reached the end.
394  if (TVRec && tv->GetEndOfRecording())
395  playCompleted = true;
396 
397  bool allowrerecord = tv->GetAllowRerecord();
398  bool deleterecording = tv->m_requestDelete;
399  AcquireRelease(refs, false);
402 
403  if (curProgram)
404  {
405  if (startSysEventSent)
406  SendMythSystemPlayEvent("PLAY_STOPPED", curProgram);
407 
408  if (deleterecording)
409  {
410  QStringList list;
411  list.push_back(QString::number(curProgram->GetRecordingID()));
412  list.push_back("0"); // do not force delete
413  list.push_back(allowrerecord ? "1" : "0");
414  MythEvent me("LOCAL_PBB_DELETE_RECORDINGS", list);
415  gCoreContext->dispatch(me);
416  }
417  else if (curProgram->IsRecording())
418  {
419  lastProgramStringList.clear();
420  curProgram->ToStringList(lastProgramStringList);
421  }
422 
423  delete curProgram;
424  }
425  else if (startSysEventSent)
426  gCoreContext->SendSystemEvent("PLAY_STOPPED");
427 
428  if (!playerError.isEmpty())
429  {
430  MythScreenStack *ss = GetMythMainWindow()->GetStack("popup stack");
431  auto *dlg = new MythConfirmationDialog(ss, playerError, false);
432  if (!dlg->Create())
433  delete dlg;
434  else
435  ss->AddScreen(dlg);
436  }
437 
438  if (startLivetvEventSent)
439  gCoreContext->SendSystemEvent("LIVETV_ENDED");
440 
441  LOG(VB_PLAYBACK, LOG_DEBUG, LOC + "-- end");
442 
443  return playCompleted;
444 }
445 
450 void TV::SetFuncPtr(const char* Name, void* Pointer)
451 {
452  QString name(Name);
453  if (name == "playbackbox")
454  RunPlaybackBoxPtr = reinterpret_cast<EMBEDRETURNVOID>(Pointer);
455  else if (name == "viewscheduled")
456  RunViewScheduledPtr = reinterpret_cast<EMBEDRETURNVOID>(Pointer);
457  else if (name == "programguide")
458  RunProgramGuidePtr = reinterpret_cast<EMBEDRETURNVOIDEPG>(Pointer);
459  else if (name == "programfinder")
460  RunProgramFinderPtr = reinterpret_cast<EMBEDRETURNVOIDFINDER>(Pointer);
461  else if (name == "scheduleeditor")
462  RunScheduleEditorPtr = reinterpret_cast<EMBEDRETURNVOIDSCHEDIT>(Pointer);
463 }
464 
466 {
467  REG_KEY("TV Frontend", ACTION_PLAYBACK, QT_TRANSLATE_NOOP("MythControls",
468  "Play Program"), "P");
469  REG_KEY("TV Frontend", ACTION_STOP, QT_TRANSLATE_NOOP("MythControls",
470  "Stop Program"), "");
471  REG_KEY("TV Frontend", ACTION_TOGGLERECORD, QT_TRANSLATE_NOOP("MythControls",
472  "Toggle recording status of current program"), "R");
473  REG_KEY("TV Frontend", ACTION_DAYLEFT, QT_TRANSLATE_NOOP("MythControls",
474  "Page the program guide back one day"), "Home");
475  REG_KEY("TV Frontend", ACTION_DAYRIGHT, QT_TRANSLATE_NOOP("MythControls",
476  "Page the program guide forward one day"), "End");
477  REG_KEY("TV Frontend", ACTION_PAGELEFT, QT_TRANSLATE_NOOP("MythControls",
478  "Page the program guide left"), ",,<");
479  REG_KEY("TV Frontend", ACTION_PAGERIGHT, QT_TRANSLATE_NOOP("MythControls",
480  "Page the program guide right"), ">,.");
481  REG_KEY("TV Frontend", ACTION_TOGGLEFAV, QT_TRANSLATE_NOOP("MythControls",
482  "Toggle the current channel as a favorite"), "?");
483  REG_KEY("TV Frontend", ACTION_TOGGLEPGORDER, QT_TRANSLATE_NOOP("MythControls",
484  "Reverse the channel order in the program guide"), "");
485  REG_KEY("TV Frontend", ACTION_GUIDE, QT_TRANSLATE_NOOP("MythControls",
486  "Show the Program Guide"), "S");
487  REG_KEY("TV Frontend", ACTION_FINDER, QT_TRANSLATE_NOOP("MythControls",
488  "Show the Program Finder"), "#");
489  REG_KEY("TV Frontend", ACTION_CHANNELSEARCH, QT_TRANSLATE_NOOP("MythControls",
490  "Show the Channel Search"), "");
491  REG_KEY("TV Frontend", "NEXTFAV", QT_TRANSLATE_NOOP("MythControls",
492  "Cycle through channel groups and all channels in the "
493  "program guide."), "/");
494  REG_KEY("TV Frontend", "CHANUPDATE", QT_TRANSLATE_NOOP("MythControls",
495  "Switch channels without exiting guide in Live TV mode."), "X");
496  REG_KEY("TV Frontend", ACTION_VOLUMEDOWN, QT_TRANSLATE_NOOP("MythControls",
497  "Volume down"), "[,{,F10,Volume Down");
498  REG_KEY("TV Frontend", ACTION_VOLUMEUP, QT_TRANSLATE_NOOP("MythControls",
499  "Volume up"), "],},F11,Volume Up");
500  REG_KEY("TV Frontend", ACTION_MUTEAUDIO, QT_TRANSLATE_NOOP("MythControls",
501  "Mute"), "|,\\,F9,Volume Mute");
502  REG_KEY("TV Frontend", "CYCLEAUDIOCHAN", QT_TRANSLATE_NOOP("MythControls",
503  "Cycle audio channels"), "");
504  REG_KEY("TV Frontend", "RANKINC", QT_TRANSLATE_NOOP("MythControls",
505  "Increase program or channel rank"), "Right");
506  REG_KEY("TV Frontend", "RANKDEC", QT_TRANSLATE_NOOP("MythControls",
507  "Decrease program or channel rank"), "Left");
508  REG_KEY("TV Frontend", "UPCOMING", QT_TRANSLATE_NOOP("MythControls",
509  "List upcoming episodes"), "O");
510  REG_KEY("TV Frontend", ACTION_VIEWSCHEDULED, QT_TRANSLATE_NOOP("MythControls",
511  "List scheduled upcoming episodes"), "");
512  REG_KEY("TV Frontend", ACTION_PREVRECORDED, QT_TRANSLATE_NOOP("MythControls",
513  "List previously recorded episodes"), "");
514  REG_KEY("TV Frontend", "DETAILS", QT_TRANSLATE_NOOP("MythControls",
515  "Show details"), "U");
516  REG_KEY("TV Frontend", "VIEWINPUT", QT_TRANSLATE_NOOP("MythControls",
517  "Switch Recording Input view"), "C");
518  REG_KEY("TV Frontend", "CUSTOMEDIT", QT_TRANSLATE_NOOP("MythControls",
519  "Edit Custom Record Rule"), "");
520  REG_KEY("TV Frontend", "CHANGERECGROUP", QT_TRANSLATE_NOOP("MythControls",
521  "Change Recording Group"), "");
522  REG_KEY("TV Frontend", "CHANGEGROUPVIEW", QT_TRANSLATE_NOOP("MythControls",
523  "Change Group View"), "");
524  REG_KEY("TV Frontend", ACTION_LISTRECORDEDEPISODES, QT_TRANSLATE_NOOP("MythControls",
525  "List recorded episodes"), "");
526  /*
527  * TODO DB update needs to perform the necessary conversion and delete
528  * the following upgrade code and replace bkmKeys and togBkmKeys with "" in the
529  * REG_KEY for ACTION_SETBOOKMARK and ACTION_TOGGLEBOOKMARK.
530  */
531  // Bookmarks - Instead of SELECT to add or toggle,
532  // Use separate bookmark actions. This code is to convert users
533  // who may already be using SELECT. If they are not already using
534  // this frontend then nothing will be assigned to bookmark actions.
535  QString bkmKeys;
536  QString togBkmKeys;
537  // Check if this is a new frontend - if PAUSE returns
538  // "?" then frontend is new, never used before, so we will not assign
539  // any default bookmark keys
540  QString testKey = MythMainWindow::GetKey("TV Playback", ACTION_PAUSE);
541  if (testKey != "?")
542  {
543  int alternate = gCoreContext->GetNumSetting("AltClearSavedPosition",0);
544  QString selectKeys = MythMainWindow::GetKey("Global", ACTION_SELECT);
545  if (selectKeys != "?")
546  {
547  if (alternate)
548  togBkmKeys = selectKeys;
549  else
550  bkmKeys = selectKeys;
551  }
552  }
553  REG_KEY("TV Playback", ACTION_SETBOOKMARK, QT_TRANSLATE_NOOP("MythControls",
554  "Add Bookmark"), bkmKeys);
555  REG_KEY("TV Playback", ACTION_TOGGLEBOOKMARK, QT_TRANSLATE_NOOP("MythControls",
556  "Toggle Bookmark"), togBkmKeys);
557  REG_KEY("TV Playback", "BACK", QT_TRANSLATE_NOOP("MythControls",
558  "Exit or return to DVD menu"), "Esc");
559  REG_KEY("TV Playback", ACTION_MENUCOMPACT, QT_TRANSLATE_NOOP("MythControls",
560  "Playback Compact Menu"), "Alt+M");
561  REG_KEY("TV Playback", ACTION_CLEAROSD, QT_TRANSLATE_NOOP("MythControls",
562  "Clear OSD"), "Backspace");
563  REG_KEY("TV Playback", ACTION_PAUSE, QT_TRANSLATE_NOOP("MythControls",
564  "Pause"), "P,Space");
565  REG_KEY("TV Playback", ACTION_SEEKFFWD, QT_TRANSLATE_NOOP("MythControls",
566  "Fast Forward"), "Right");
567  REG_KEY("TV Playback", ACTION_SEEKRWND, QT_TRANSLATE_NOOP("MythControls",
568  "Rewind"), "Left");
569  REG_KEY("TV Playback", ACTION_SEEKARB, QT_TRANSLATE_NOOP("MythControls",
570  "Arbitrary Seek"), "*");
571  REG_KEY("TV Playback", ACTION_SEEKABSOLUTE, QT_TRANSLATE_NOOP("MythControls",
572  "Seek to a position in seconds"), "");
573  REG_KEY("TV Playback", ACTION_CHANNELUP, QT_TRANSLATE_NOOP("MythControls",
574  "Channel up"), "Up");
575  REG_KEY("TV Playback", ACTION_CHANNELDOWN, QT_TRANSLATE_NOOP("MythControls",
576  "Channel down"), "Down");
577  REG_KEY("TV Playback", "NEXTFAV", QT_TRANSLATE_NOOP("MythControls",
578  "Switch to the next favorite channel"), "/");
579  REG_KEY("TV Playback", "PREVCHAN", QT_TRANSLATE_NOOP("MythControls",
580  "Switch to the previous channel"), "H");
581  REG_KEY("TV Playback", ACTION_JUMPFFWD, QT_TRANSLATE_NOOP("MythControls",
582  "Jump ahead"), "PgDown");
583  REG_KEY("TV Playback", ACTION_JUMPRWND, QT_TRANSLATE_NOOP("MythControls",
584  "Jump back"), "PgUp");
585  REG_KEY("TV Playback", "INFOWITHCUTLIST", QT_TRANSLATE_NOOP("MythControls",
586  "Info utilizing cutlist"), "");
587  REG_KEY("TV Playback", ACTION_JUMPBKMRK, QT_TRANSLATE_NOOP("MythControls",
588  "Jump to bookmark"), "K");
589  REG_KEY("TV Playback", "FFWDSTICKY", QT_TRANSLATE_NOOP("MythControls",
590  "Fast Forward (Sticky) or Forward one second while paused"), ">,.");
591  REG_KEY("TV Playback", "RWNDSTICKY", QT_TRANSLATE_NOOP("MythControls",
592  "Rewind (Sticky) or Rewind one second while paused"), ",,<");
593  REG_KEY("TV Playback", "NEXTSOURCE", QT_TRANSLATE_NOOP("MythControls",
594  "Next Video Source"), "Y");
595  REG_KEY("TV Playback", "PREVSOURCE", QT_TRANSLATE_NOOP("MythControls",
596  "Previous Video Source"), "");
597  REG_KEY("TV Playback", "NEXTINPUT", QT_TRANSLATE_NOOP("MythControls",
598  "Next Input"), "C");
599  REG_KEY("TV Playback", "NEXTCARD", QT_TRANSLATE_NOOP("MythControls",
600  "Next Card"), "");
601  REG_KEY("TV Playback", "SKIPCOMMERCIAL", QT_TRANSLATE_NOOP("MythControls",
602  "Skip Commercial"), "Z,End");
603  REG_KEY("TV Playback", "SKIPCOMMBACK", QT_TRANSLATE_NOOP("MythControls",
604  "Skip Commercial (Reverse)"), "Q,Home");
605  REG_KEY("TV Playback", ACTION_JUMPSTART, QT_TRANSLATE_NOOP("MythControls",
606  "Jump to the start of the recording."), "Ctrl+B");
607  REG_KEY("TV Playback", "TOGGLEBROWSE", QT_TRANSLATE_NOOP("MythControls",
608  "Toggle channel browse mode"), "O");
609  REG_KEY("TV Playback", ACTION_TOGGLERECORD, QT_TRANSLATE_NOOP("MythControls",
610  "Toggle recording status of current program"), "R");
611  REG_KEY("TV Playback", ACTION_TOGGLEFAV, QT_TRANSLATE_NOOP("MythControls",
612  "Toggle the current channel as a favorite"), "?");
613  REG_KEY("TV Playback", ACTION_VOLUMEDOWN, QT_TRANSLATE_NOOP("MythControls",
614  "Volume down"), "[,{,F10,Volume Down");
615  REG_KEY("TV Playback", ACTION_VOLUMEUP, QT_TRANSLATE_NOOP("MythControls",
616  "Volume up"), "],},F11,Volume Up");
617  REG_KEY("TV Playback", ACTION_MUTEAUDIO, QT_TRANSLATE_NOOP("MythControls",
618  "Mute"), "|,\\,F9,Volume Mute");
619  REG_KEY("TV Playback", ACTION_SETVOLUME, QT_TRANSLATE_NOOP("MythControls",
620  "Set the volume"), "");
621  REG_KEY("TV Playback", "CYCLEAUDIOCHAN", QT_TRANSLATE_NOOP("MythControls",
622  "Cycle audio channels"), "");
623  REG_KEY("TV Playback", ACTION_TOGGLEUPMIX, QT_TRANSLATE_NOOP("MythControls",
624  "Toggle audio upmixer"), "Ctrl+U");
625  REG_KEY("TV Playback", ACTION_BOTTOMLINEMOVE,
626  QT_TRANSLATE_NOOP("MythControls", "Move BottomLine off screen"), "L");
627  REG_KEY("TV Playback", ACTION_BOTTOMLINESAVE,
628  QT_TRANSLATE_NOOP("MythControls", "Save manual zoom for BottomLine"), "");
629  REG_KEY("TV Playback", "TOGGLEASPECT", QT_TRANSLATE_NOOP("MythControls",
630  "Toggle the video aspect ratio"), "Ctrl+W");
631  REG_KEY("TV Playback", "TOGGLEFILL", QT_TRANSLATE_NOOP("MythControls",
632  "Next Preconfigured Zoom mode"), "W");
633  REG_KEY("TV Playback", ACTION_TOGGLESUBS, QT_TRANSLATE_NOOP("MythControls",
634  "Toggle any captions"), "T");
635  REG_KEY("TV Playback", ACTION_ENABLESUBS, QT_TRANSLATE_NOOP("MythControls",
636  "Enable any captions"), "");
637  REG_KEY("TV Playback", ACTION_DISABLESUBS, QT_TRANSLATE_NOOP("MythControls",
638  "Disable any captions"), "");
639  REG_KEY("TV Playback", "TOGGLETTC", QT_TRANSLATE_NOOP("MythControls",
640  "Toggle Teletext Captions"),"");
641  REG_KEY("TV Playback", "TOGGLESUBTITLE", QT_TRANSLATE_NOOP("MythControls",
642  "Toggle Subtitles"), "");
643  REG_KEY("TV Playback", "TOGGLECC608", QT_TRANSLATE_NOOP("MythControls",
644  "Toggle VBI CC"), "");
645  REG_KEY("TV Playback", "TOGGLECC708", QT_TRANSLATE_NOOP("MythControls",
646  "Toggle ATSC CC"), "");
647  REG_KEY("TV Playback", "TOGGLETTM", QT_TRANSLATE_NOOP("MythControls",
648  "Toggle Teletext Menu"), "");
649  REG_KEY("TV Playback", ACTION_TOGGLEEXTTEXT, QT_TRANSLATE_NOOP("MythControls",
650  "Toggle External Subtitles"), "");
651  REG_KEY("TV Playback", ACTION_ENABLEEXTTEXT, QT_TRANSLATE_NOOP("MythControls",
652  "Enable External Subtitles"), "");
653  REG_KEY("TV Playback", ACTION_DISABLEEXTTEXT, QT_TRANSLATE_NOOP("MythControls",
654  "Disable External Subtitles"), "");
655  REG_KEY("TV Playback", "TOGGLERAWTEXT", QT_TRANSLATE_NOOP("MythControls",
656  "Toggle Text Subtitles"), "");
657 
658  REG_KEY("TV Playback", "SELECTAUDIO_0", QT_TRANSLATE_NOOP("MythControls",
659  "Play audio track 1"), "");
660  REG_KEY("TV Playback", "SELECTAUDIO_1", QT_TRANSLATE_NOOP("MythControls",
661  "Play audio track 2"), "");
662  REG_KEY("TV Playback", "SELECTSUBTITLE_0",QT_TRANSLATE_NOOP("MythControls",
663  "Display subtitle 1"), "");
664  REG_KEY("TV Playback", "SELECTSUBTITLE_1",QT_TRANSLATE_NOOP("MythControls",
665  "Display subtitle 2"), "");
666  REG_KEY("TV Playback", "SELECTRAWTEXT_0",QT_TRANSLATE_NOOP("MythControls",
667  "Display Text Subtitle 1"), "");
668  REG_KEY("TV Playback", "SELECTCC608_0", QT_TRANSLATE_NOOP("MythControls",
669  "Display VBI CC1"), "");
670  REG_KEY("TV Playback", "SELECTCC608_1", QT_TRANSLATE_NOOP("MythControls",
671  "Display VBI CC2"), "");
672  REG_KEY("TV Playback", "SELECTCC608_2", QT_TRANSLATE_NOOP("MythControls",
673  "Display VBI CC3"), "");
674  REG_KEY("TV Playback", "SELECTCC608_3", QT_TRANSLATE_NOOP("MythControls",
675  "Display VBI CC4"), "");
676  REG_KEY("TV Playback", "SELECTCC708_0", QT_TRANSLATE_NOOP("MythControls",
677  "Display ATSC CC1"), "");
678  REG_KEY("TV Playback", "SELECTCC708_1", QT_TRANSLATE_NOOP("MythControls",
679  "Display ATSC CC2"), "");
680  REG_KEY("TV Playback", "SELECTCC708_2", QT_TRANSLATE_NOOP("MythControls",
681  "Display ATSC CC3"), "");
682  REG_KEY("TV Playback", "SELECTCC708_3", QT_TRANSLATE_NOOP("MythControls",
683  "Display ATSC CC4"), "");
684  REG_KEY("TV Playback", ACTION_ENABLEFORCEDSUBS, QT_TRANSLATE_NOOP("MythControls",
685  "Enable Forced Subtitles"), "");
686  REG_KEY("TV Playback", ACTION_DISABLEFORCEDSUBS, QT_TRANSLATE_NOOP("MythControls",
687  "Disable Forced Subtitles"), "");
688 
689  REG_KEY("TV Playback", "NEXTAUDIO", QT_TRANSLATE_NOOP("MythControls",
690  "Next audio track"), "+");
691  REG_KEY("TV Playback", "PREVAUDIO", QT_TRANSLATE_NOOP("MythControls",
692  "Previous audio track"), "-");
693  REG_KEY("TV Playback", "NEXTSUBTITLE", QT_TRANSLATE_NOOP("MythControls",
694  "Next subtitle track"), "");
695  REG_KEY("TV Playback", "PREVSUBTITLE", QT_TRANSLATE_NOOP("MythControls",
696  "Previous subtitle track"), "");
697  REG_KEY("TV Playback", "NEXTRAWTEXT", QT_TRANSLATE_NOOP("MythControls",
698  "Next Text track"), "");
699  REG_KEY("TV Playback", "PREVRAWTEXT", QT_TRANSLATE_NOOP("MythControls",
700  "Previous Text track"), "");
701  REG_KEY("TV Playback", "NEXTCC608", QT_TRANSLATE_NOOP("MythControls",
702  "Next VBI CC track"), "");
703  REG_KEY("TV Playback", "PREVCC608", QT_TRANSLATE_NOOP("MythControls",
704  "Previous VBI CC track"), "");
705  REG_KEY("TV Playback", "NEXTCC708", QT_TRANSLATE_NOOP("MythControls",
706  "Next ATSC CC track"), "");
707  REG_KEY("TV Playback", "PREVCC708", QT_TRANSLATE_NOOP("MythControls",
708  "Previous ATSC CC track"), "");
709  REG_KEY("TV Playback", "NEXTCC", QT_TRANSLATE_NOOP("MythControls",
710  "Next of any captions"), "");
711 
712  REG_KEY("TV Playback", "NEXTSCAN", QT_TRANSLATE_NOOP("MythControls",
713  "Next video scan overidemode"), "");
714  REG_KEY("TV Playback", "QUEUETRANSCODE", QT_TRANSLATE_NOOP("MythControls",
715  "Queue the current recording for transcoding"), "X");
716  REG_KEY("TV Playback", "SPEEDINC", QT_TRANSLATE_NOOP("MythControls",
717  "Increase the playback speed"), "U");
718  REG_KEY("TV Playback", "SPEEDDEC", QT_TRANSLATE_NOOP("MythControls",
719  "Decrease the playback speed"), "J");
720  REG_KEY("TV Playback", "ADJUSTSTRETCH", QT_TRANSLATE_NOOP("MythControls",
721  "Turn on time stretch control"), "A");
722  REG_KEY("TV Playback", "STRETCHINC", QT_TRANSLATE_NOOP("MythControls",
723  "Increase time stretch speed"), "");
724  REG_KEY("TV Playback", "STRETCHDEC", QT_TRANSLATE_NOOP("MythControls",
725  "Decrease time stretch speed"), "");
726  REG_KEY("TV Playback", "TOGGLESTRETCH", QT_TRANSLATE_NOOP("MythControls",
727  "Toggle time stretch speed"), "");
728  REG_KEY("TV Playback", ACTION_TOGGELAUDIOSYNC,
729  QT_TRANSLATE_NOOP("MythControls",
730  "Turn on audio sync adjustment controls"), "");
731  REG_KEY("TV Playback", ACTION_SETAUDIOSYNC,
732  QT_TRANSLATE_NOOP("MythControls",
733  "Set the audio sync adjustment"), "");
734  REG_KEY("TV Playback", "TOGGLEPICCONTROLS",
735  QT_TRANSLATE_NOOP("MythControls", "Playback picture adjustments"),
736  "F");
737  REG_KEY("TV Playback", ACTION_SETBRIGHTNESS,
738  QT_TRANSLATE_NOOP("MythControls", "Set the picture brightness"), "");
739  REG_KEY("TV Playback", ACTION_SETCONTRAST,
740  QT_TRANSLATE_NOOP("MythControls", "Set the picture contrast"), "");
741  REG_KEY("TV Playback", ACTION_SETCOLOUR,
742  QT_TRANSLATE_NOOP("MythControls", "Set the picture color"), "");
743  REG_KEY("TV Playback", ACTION_SETHUE,
744  QT_TRANSLATE_NOOP("MythControls", "Set the picture hue"), "");
745  REG_KEY("TV Playback", ACTION_TOGGLECHANCONTROLS,
746  QT_TRANSLATE_NOOP("MythControls", "Recording picture adjustments "
747  "for this channel"), "Ctrl+G");
748  REG_KEY("TV Playback", ACTION_TOGGLERECCONTROLS,
749  QT_TRANSLATE_NOOP("MythControls", "Recording picture adjustments "
750  "for this recorder"), "G");
751  REG_KEY("TV Playback", "CYCLECOMMSKIPMODE",
752  QT_TRANSLATE_NOOP("MythControls", "Cycle Commercial Skip mode"),
753  "");
754  REG_KEY("TV Playback", ACTION_GUIDE, QT_TRANSLATE_NOOP("MythControls",
755  "Show the Program Guide"), "S");
756  REG_KEY("TV Playback", ACTION_FINDER, QT_TRANSLATE_NOOP("MythControls",
757  "Show the Program Finder"), "#");
758  REG_KEY("TV Playback", ACTION_TOGGLESLEEP, QT_TRANSLATE_NOOP("MythControls",
759  "Toggle the Sleep Timer"), "F8");
760  REG_KEY("TV Playback", ACTION_PLAY, QT_TRANSLATE_NOOP("MythControls", "Play"),
761  "Ctrl+P");
762  REG_KEY("TV Playback", ACTION_JUMPPREV, QT_TRANSLATE_NOOP("MythControls",
763  "Jump to previously played recording"), "");
764  REG_KEY("TV Playback", ACTION_JUMPREC, QT_TRANSLATE_NOOP("MythControls",
765  "Display menu of recorded programs to jump to"), "");
766  REG_KEY("TV Playback", ACTION_VIEWSCHEDULED, QT_TRANSLATE_NOOP("MythControls",
767  "Display scheduled recording list"), "");
768  REG_KEY("TV Playback", ACTION_PREVRECORDED, QT_TRANSLATE_NOOP("MythControls",
769  "Display previously recorded episodes"), "");
770  REG_KEY("TV Playback", ACTION_SIGNALMON, QT_TRANSLATE_NOOP("MythControls",
771  "Monitor Signal Quality"), "Alt+F7");
772  REG_KEY("TV Playback", ACTION_JUMPTODVDROOTMENU,
773  QT_TRANSLATE_NOOP("MythControls", "Jump to the DVD Root Menu"), "");
774  REG_KEY("TV Playback", ACTION_JUMPTOPOPUPMENU,
775  QT_TRANSLATE_NOOP("MythControls", "Jump to the Popup Menu"), "");
776  REG_KEY("TV Playback", ACTION_JUMPTODVDCHAPTERMENU,
777  QT_TRANSLATE_NOOP("MythControls", "Jump to the DVD Chapter Menu"), "");
778  REG_KEY("TV Playback", ACTION_JUMPTODVDTITLEMENU,
779  QT_TRANSLATE_NOOP("MythControls", "Jump to the DVD Title Menu"), "");
780  REG_KEY("TV Playback", ACTION_EXITSHOWNOPROMPTS,
781  QT_TRANSLATE_NOOP("MythControls", "Exit Show without any prompts"),
782  "");
783  REG_KEY("TV Playback", ACTION_JUMPCHAPTER, QT_TRANSLATE_NOOP("MythControls",
784  "Jump to a chapter"), "");
785  REG_KEY("TV Playback", ACTION_SWITCHTITLE, QT_TRANSLATE_NOOP("MythControls",
786  "Switch title"), "");
787  REG_KEY("TV Playback", ACTION_SWITCHANGLE, QT_TRANSLATE_NOOP("MythControls",
788  "Switch angle"), "");
789  REG_KEY("TV Playback", ACTION_OSDNAVIGATION, QT_TRANSLATE_NOOP("MythControls",
790  "OSD Navigation"), "");
791  REG_KEY("TV Playback", ACTION_ZOOMUP, QT_TRANSLATE_NOOP("MythControls",
792  "Zoom mode - shift up"), "");
793  REG_KEY("TV Playback", ACTION_ZOOMDOWN, QT_TRANSLATE_NOOP("MythControls",
794  "Zoom mode - shift down"), "");
795  REG_KEY("TV Playback", ACTION_ZOOMLEFT, QT_TRANSLATE_NOOP("MythControls",
796  "Zoom mode - shift left"), "");
797  REG_KEY("TV Playback", ACTION_ZOOMRIGHT, QT_TRANSLATE_NOOP("MythControls",
798  "Zoom mode - shift right"), "");
799  REG_KEY("TV Playback", ACTION_ZOOMASPECTUP,
800  QT_TRANSLATE_NOOP("MythControls",
801  "Zoom mode - increase aspect ratio"), "");
802  REG_KEY("TV Playback", ACTION_ZOOMASPECTDOWN,
803  QT_TRANSLATE_NOOP("MythControls",
804  "Zoom mode - decrease aspect ratio"), "");
805  REG_KEY("TV Playback", ACTION_ZOOMIN, QT_TRANSLATE_NOOP("MythControls",
806  "Zoom mode - zoom in"), "");
807  REG_KEY("TV Playback", ACTION_ZOOMOUT, QT_TRANSLATE_NOOP("MythControls",
808  "Zoom mode - zoom out"), "");
809  REG_KEY("TV Playback", ACTION_ZOOMVERTICALIN,
810  QT_TRANSLATE_NOOP("MythControls",
811  "Zoom mode - vertical zoom in"), "8");
812  REG_KEY("TV Playback", ACTION_ZOOMVERTICALOUT,
813  QT_TRANSLATE_NOOP("MythControls",
814  "Zoom mode - vertical zoom out"), "2");
815  REG_KEY("TV Playback", ACTION_ZOOMHORIZONTALIN,
816  QT_TRANSLATE_NOOP("MythControls",
817  "Zoom mode - horizontal zoom in"), "6");
818  REG_KEY("TV Playback", ACTION_ZOOMHORIZONTALOUT,
819  QT_TRANSLATE_NOOP("MythControls",
820  "Zoom mode - horizontal zoom out"), "4");
821  REG_KEY("TV Playback", ACTION_ZOOMQUIT, QT_TRANSLATE_NOOP("MythControls",
822  "Zoom mode - quit and abandon changes"), "");
823  REG_KEY("TV Playback", ACTION_ZOOMCOMMIT, QT_TRANSLATE_NOOP("MythControls",
824  "Zoom mode - commit changes"), "");
825 
826  /* Interactive Television keys */
827  REG_KEY("TV Playback", ACTION_MENURED, QT_TRANSLATE_NOOP("MythControls",
828  "Menu Red"), "F2");
829  REG_KEY("TV Playback", ACTION_MENUGREEN, QT_TRANSLATE_NOOP("MythControls",
830  "Menu Green"), "F3");
831  REG_KEY("TV Playback", ACTION_MENUYELLOW, QT_TRANSLATE_NOOP("MythControls",
832  "Menu Yellow"), "F4");
833  REG_KEY("TV Playback", ACTION_MENUBLUE, QT_TRANSLATE_NOOP("MythControls",
834  "Menu Blue"), "F5");
835  REG_KEY("TV Playback", ACTION_TEXTEXIT, QT_TRANSLATE_NOOP("MythControls",
836  "Menu Exit"), "F6");
837  REG_KEY("TV Playback", ACTION_MENUTEXT, QT_TRANSLATE_NOOP("MythControls",
838  "Menu Text"), "F7");
839  REG_KEY("TV Playback", ACTION_MENUEPG, QT_TRANSLATE_NOOP("MythControls",
840  "Menu EPG"), "F12");
841 
842  /* Editing keys */
843  REG_KEY("TV Editing", ACTION_CLEARMAP, QT_TRANSLATE_NOOP("MythControls",
844  "Clear editing cut points"), "C,Q,Home");
845  REG_KEY("TV Editing", ACTION_INVERTMAP, QT_TRANSLATE_NOOP("MythControls",
846  "Invert Begin/End cut points"),"I");
847  REG_KEY("TV Editing", ACTION_SAVEMAP, QT_TRANSLATE_NOOP("MythControls",
848  "Save cuts"),"");
849  REG_KEY("TV Editing", ACTION_LOADCOMMSKIP,QT_TRANSLATE_NOOP("MythControls",
850  "Load cuts from detected commercials"), "Z,End");
851  REG_KEY("TV Editing", ACTION_NEXTCUT, QT_TRANSLATE_NOOP("MythControls",
852  "Jump to the next cut point"), "PgDown");
853  REG_KEY("TV Editing", ACTION_PREVCUT, QT_TRANSLATE_NOOP("MythControls",
854  "Jump to the previous cut point"), "PgUp");
855  REG_KEY("TV Editing", ACTION_BIGJUMPREW, QT_TRANSLATE_NOOP("MythControls",
856  "Jump back 10x the normal amount"), ",,<");
857  REG_KEY("TV Editing", ACTION_BIGJUMPFWD, QT_TRANSLATE_NOOP("MythControls",
858  "Jump forward 10x the normal amount"), ">,.");
859  REG_KEY("TV Editing", ACTION_MENUCOMPACT, QT_TRANSLATE_NOOP("MythControls",
860  "Cut point editor compact menu"), "Alt+M");
861 
862  /* Teletext keys */
863  REG_KEY("Teletext Menu", ACTION_NEXTPAGE, QT_TRANSLATE_NOOP("MythControls",
864  "Next Page"), "Down");
865  REG_KEY("Teletext Menu", ACTION_PREVPAGE, QT_TRANSLATE_NOOP("MythControls",
866  "Previous Page"), "Up");
867  REG_KEY("Teletext Menu", ACTION_NEXTSUBPAGE, QT_TRANSLATE_NOOP("MythControls",
868  "Next Subpage"), "Right");
869  REG_KEY("Teletext Menu", ACTION_PREVSUBPAGE, QT_TRANSLATE_NOOP("MythControls",
870  "Previous Subpage"), "Left");
871  REG_KEY("Teletext Menu", ACTION_TOGGLETT, QT_TRANSLATE_NOOP("MythControls",
872  "Toggle Teletext"), "T");
873  REG_KEY("Teletext Menu", ACTION_MENURED, QT_TRANSLATE_NOOP("MythControls",
874  "Menu Red"), "F2");
875  REG_KEY("Teletext Menu", ACTION_MENUGREEN, QT_TRANSLATE_NOOP("MythControls",
876  "Menu Green"), "F3");
877  REG_KEY("Teletext Menu", ACTION_MENUYELLOW, QT_TRANSLATE_NOOP("MythControls",
878  "Menu Yellow"), "F4");
879  REG_KEY("Teletext Menu", ACTION_MENUBLUE, QT_TRANSLATE_NOOP("MythControls",
880  "Menu Blue"), "F5");
881  REG_KEY("Teletext Menu", ACTION_MENUWHITE, QT_TRANSLATE_NOOP("MythControls",
882  "Menu White"), "F6");
883  REG_KEY("Teletext Menu", ACTION_TOGGLEBACKGROUND,
884  QT_TRANSLATE_NOOP("MythControls", "Toggle Background"), "F7");
885  REG_KEY("Teletext Menu", ACTION_REVEAL, QT_TRANSLATE_NOOP("MythControls",
886  "Reveal hidden Text"), "F8");
887 
888  /* Visualisations */
889  REG_KEY("TV Playback", ACTION_TOGGLEVISUALISATION,
890  QT_TRANSLATE_NOOP("MythControls", "Toggle audio visualisation"), "");
891 
892  /* OSD playback information screen */
893  REG_KEY("TV Playback", ACTION_TOGGLEOSDDEBUG,
894  QT_TRANSLATE_NOOP("MythControls", "Toggle OSD playback information"), "");
895 
896  /* 3D/Frame compatible/Stereoscopic TV */
897  REG_KEY("TV Playback", ACTION_3DNONE,
898  QT_TRANSLATE_NOOP("MythControls", "Auto 3D"), "");
899  REG_KEY("TV Playback", ACTION_3DIGNORE,
900  QT_TRANSLATE_NOOP("MythControls", "Ignore 3D"), "");
901  REG_KEY("TV Playback", ACTION_3DSIDEBYSIDEDISCARD,
902  QT_TRANSLATE_NOOP("MythControls", "Discard 3D Side by Side"), "");
903  REG_KEY("TV Playback", ACTION_3DTOPANDBOTTOMDISCARD,
904  QT_TRANSLATE_NOOP("MythControls", "Discard 3D Top and Bottom"), "");
905 
906 /*
907  keys already used:
908 
909  Global: I M 0123456789
910  Playback: ABCDEFGH JK NOPQRSTUVWXYZ
911  Frontend: CD OP R U XY 01 3 7 9
912  Editing: C E I Q Z
913  Teletext: T
914 
915  Playback: <>,.?/|[]{}\+-*#^
916  Frontend: <>,.?/
917  Editing: <>,.
918 
919  Global: PgDown, PgUp, Right, Left, Home, End, Up, Down,
920  Playback: PgDown, PgUp, Right, Left, Home, End, Up, Down, Backspace,
921  Frontend: Right, Left, Home, End
922  Editing: PgDown, PgUp, Home, End
923  Teletext: Right, Left, Up, Down,
924 
925  Global: Return, Enter, Space, Esc
926 
927  Global: F1,
928  Playback: F7,F8,F9,F10,F11
929  Teletext F2,F3,F4,F5,F6,F7,F8
930  ITV F2,F3,F4,F5,F6,F7,F12
931 
932  Playback: Ctrl-B,Ctrl-G,Ctrl-Y,Ctrl-U,L
933 */
934 }
935 
937 {
938  m_mainWindow->ClearKeyContext("TV Frontend");
939  m_mainWindow->ClearKeyContext("TV Playback");
940  m_mainWindow->ClearKeyContext("TV Editing");
941  m_mainWindow->ClearKeyContext("Teletext Menu");
942  InitKeys();
943 }
944 
945 
947 {
948  public:
949  SleepTimerInfo(QString String, std::chrono::milliseconds MilliSeconds)
950  : dispString(std::move(String)),
951  milliseconds(MilliSeconds) {}
952  QString dispString;
953  std::chrono::milliseconds milliseconds;
954 };
955 
956 const std::vector<TV::SleepTimerInfo> TV::s_sleepTimes =
957 {
958  { tr("Off", "Sleep timer"), 0min },
959  { tr("30m", "Sleep timer"), 30min },
960  { tr("1h", "Sleep timer"), 60min },
961  { tr("1h30m", "Sleep timer"), 90min },
962  { tr("2h", "Sleep timer"), 120min }
963 };
964 
976 TV::TV(MythMainWindow* MainWindow)
977  : ReferenceCounter("TV"),
978  TVBrowseHelper(this),
979  m_mainWindow(MainWindow)
980 
981 {
982  LOG(VB_GENERAL, LOG_INFO, LOC + "Creating TV object");
983 
984  QObject::setObjectName("TV");
986  connect(this, &TV::RequestEmbedding, this, &TV::Embed);
987  InitFromDB();
988 
989 #ifdef Q_OS_ANDROID
990  connect(qApp, &QApplication::applicationStateChanged, this, &TV::onApplicationStateChange);
991 #endif
992 
993  if (m_mainWindow)
995 
996  // Setup various state signals
997  connect(this, &TV::ChangeAudioOffset, [&]() { m_audiosyncAdjustment = true; });
998  connect(this, &TV::AdjustSubtitleZoom, [&]() { m_subtitleZoomAdjustment = true; });
999  connect(this, &TV::AdjustSubtitleDelay, [&]() { m_subtitleDelayAdjustment = true; });
1000 
1001  LOG(VB_PLAYBACK, LOG_INFO, LOC + "Finished creating TV object");
1002 }
1003 
1005 {
1006  QMap<QString,QString> kv;
1007  kv["LiveTVIdleTimeout"] = "0";
1008  kv["BrowseMaxForward"] = "240";
1009  kv["PlaybackExitPrompt"] = "0";
1010  kv["AutomaticSetWatched"] = "0";
1011  kv["EndOfRecordingExitPrompt"] = "0";
1012  kv["JumpToProgramOSD"] = "1";
1013  kv["GuiSizeForTV"] = "0";
1014  kv["UseVideoModes"] = "0";
1015  kv["JobsRunOnRecordHost"] = "0";
1016  kv["ContinueEmbeddedTVPlay"] = "0";
1017  kv["UseFixedWindowSize"] = "1";
1018  kv["RunFrontendInWindow"] = "0";
1019  kv["PersistentBrowseMode"] = "0";
1020  kv["BrowseAllTuners"] = "0";
1021  kv["ChannelOrdering"] = "channum";
1022 
1023  kv["CustomFilters"] = "";
1024  kv["ChannelFormat"] = "<num> <sign>";
1025 
1026  kv["TryUnflaggedSkip"] = "0";
1027 
1028  kv["ChannelGroupDefault"] = "-1";
1029  kv["BrowseChannelGroup"] = "0";
1030  kv["SmartForward"] = "0";
1031  kv["FFRewReposTime"] = "100";
1032  kv["FFRewReverse"] = "1";
1033 
1034  kv["BrowseChannelGroup"] = "0";
1035  kv["ChannelGroupDefault"] = "-1";
1036  kv["ChannelGroupRememberLast"] = "0";
1037 
1038  kv["VbiFormat"] = "";
1039  kv["DecodeVBIFormat"] = "";
1040 
1041  // these need exactly 12 items, comma cant be used as it is the delimiter
1042  kv["PlaybackScreenPressKeyMap"] = "P,Up,Z,],Left,Return,Return,Right,A,Down,Q,[";
1043  kv["LiveTVScreenPressKeyMap"] = "P,Up,Z,S,Left,Return,Return,Right,A,Down,Q,F";
1044 
1045  constexpr std::array<const int,8> ff_rew_def { 3, 5, 10, 20, 30, 60, 120, 180 };
1046  for (size_t i = 0; i < ff_rew_def.size(); i++)
1047  kv[QString("FFRewSpeed%1").arg(i)] = QString::number(ff_rew_def[i]);
1048 
1049  MythDB::getMythDB()->GetSettings(kv);
1050 
1051  m_screenPressKeyMapPlayback = ConvertScreenPressKeyMap(kv["PlaybackScreenPressKeyMap"]);
1052  m_screenPressKeyMapLiveTV = ConvertScreenPressKeyMap(kv["LiveTVScreenPressKeyMap"]);
1053 
1054  QString db_channel_ordering;
1055 
1056  m_dbIdleTimeout = std::chrono::minutes(kv["LiveTVIdleTimeout"].toUInt());
1057  auto db_browse_max_forward = std::chrono::minutes(kv["BrowseMaxForward"].toUInt());
1058  m_dbPlaybackExitPrompt = kv["PlaybackExitPrompt"].toInt();
1059  m_dbAutoSetWatched = (kv["AutomaticSetWatched"].toInt() != 0);
1060  m_dbEndOfRecExitPrompt = (kv["EndOfRecordingExitPrompt"].toInt() != 0);
1061  m_dbJumpPreferOsd = (kv["JumpToProgramOSD"].toInt() != 0);
1062  m_dbUseGuiSizeForTv = (kv["GuiSizeForTV"].toInt() != 0);
1063  m_dbUseVideoModes = (kv["UseVideoModes"].toInt() != 0);
1064  m_dbRunJobsOnRemote = (kv["JobsRunOnRecordHost"].toInt() != 0);
1065  m_dbContinueEmbedded = (kv["ContinueEmbeddedTVPlay"].toInt() != 0);
1066  m_dbBrowseAlways = (kv["PersistentBrowseMode"].toInt() != 0);
1067  m_dbBrowseAllTuners = (kv["BrowseAllTuners"].toInt() != 0);
1068  db_channel_ordering = kv["ChannelOrdering"];
1069  m_dbChannelFormat = kv["ChannelFormat"];
1070  m_smartForward = (kv["SmartForward"].toInt() != 0);
1071  m_ffRewRepos = kv["FFRewReposTime"].toFloat() * 0.01F;
1072  m_ffRewReverse = (kv["FFRewReverse"].toInt() != 0);
1073 
1074  m_dbUseChannelGroups = (kv["BrowseChannelGroup"].toInt() != 0);
1075  m_dbRememberLastChannelGroup = (kv["ChannelGroupRememberLast"].toInt() != 0);
1076  m_channelGroupId = kv["ChannelGroupDefault"].toInt();
1077 
1078  QString beVBI = kv["VbiFormat"];
1079  QString feVBI = kv["DecodeVBIFormat"];
1080 
1081  RecordingRule record;
1082  record.LoadTemplate("Default");
1083  m_dbAutoexpireDefault = static_cast<uint>(record.m_autoExpire);
1084 
1086  {
1088  if (m_channelGroupId > -1)
1089  {
1090  m_channelGroupChannelList = ChannelUtil::GetChannels(0, true, "channum, callsign",
1091  static_cast<uint>(m_channelGroupId));
1093  }
1094  }
1095 
1096  for (size_t i = 0; i < sizeof(ff_rew_def)/sizeof(ff_rew_def[0]); i++)
1097  m_ffRewSpeeds.push_back(kv[QString("FFRewSpeed%1").arg(i)].toInt());
1098 
1099  // process it..
1100  BrowseInit(db_browse_max_forward, m_dbBrowseAllTuners, m_dbUseChannelGroups, db_channel_ordering);
1101 
1102  m_vbimode = VBIMode::Parse(!feVBI.isEmpty() ? feVBI : beVBI);
1103 
1104  gCoreContext->addListener(this);
1106 }
1107 
1112 bool TV::Init()
1113 {
1114  LOG(VB_PLAYBACK, LOG_DEBUG, LOC + "-- begin");
1115 
1116  if (!m_mainWindow)
1117  {
1118  LOG(VB_GENERAL, LOG_ERR, LOC + "No MythMainWindow");
1119  return false;
1120  }
1121 
1122  bool fullscreen = !m_dbUseGuiSizeForTv;
1123  m_savedGuiBounds = QRect(m_mainWindow->geometry().topLeft(), m_mainWindow->size());
1124 
1125  // adjust for window manager wierdness.
1126  QRect screen = m_mainWindow->GetScreenRect();
1127  if ((abs(m_savedGuiBounds.x() - screen.left()) < 3) &&
1128  (abs(m_savedGuiBounds.y() - screen.top()) < 3))
1129  {
1130  m_savedGuiBounds = QRect(screen.topLeft(), m_mainWindow->size());
1131  }
1132 
1133  // if width && height are zero users expect fullscreen playback
1134  if (!fullscreen)
1135  {
1136  int gui_width = 0;
1137  int gui_height = 0;
1138  gCoreContext->GetResolutionSetting("Gui", gui_width, gui_height);
1139  fullscreen |= (0 == gui_width && 0 == gui_height);
1140  }
1141 
1143  if (fullscreen)
1145 
1146  // player window sizing
1147  MythScreenStack *mainStack = m_mainWindow->GetMainStack();
1148 
1149  m_myWindow = new TvPlayWindow(mainStack, "Playback");
1150 
1151  if (m_myWindow->Create())
1152  {
1153  mainStack->AddScreen(m_myWindow, false);
1154  LOG(VB_GENERAL, LOG_INFO, LOC + "Created TvPlayWindow.");
1155  }
1156  else
1157  {
1158  delete m_myWindow;
1159  m_myWindow = nullptr;
1160  }
1161 
1163  m_mainWindow->GetPaintWindow()->update();
1164  m_mainWindow->installEventFilter(this);
1165  QCoreApplication::processEvents();
1166 
1171  m_playerContext.m_tsNormal = 1.0F;
1172  ReturnPlayerLock();
1173 
1174  m_sleepIndex = 0;
1175 
1176  emit ChangeOSDPositionUpdates(false);
1177 
1179  ClearInputQueues(false);
1180  ReturnPlayerLock();
1181 
1182  m_switchToRec = nullptr;
1183  SetExitPlayer(false, false);
1184 
1186  m_lcdTimerId = StartTimer(1ms, __LINE__);
1189 
1190  LOG(VB_PLAYBACK, LOG_DEBUG, LOC + "-- end");
1191  return true;
1192 }
1193 
1195 {
1196  LOG(VB_PLAYBACK, LOG_DEBUG, LOC + "-- begin");
1197 
1198  BrowseStop();
1199  BrowseWait();
1200 
1203 
1204  if (m_mainWindow)
1205  {
1206  m_mainWindow->removeEventFilter(this);
1207  if (m_weDisabledGUI)
1209  }
1210 
1211  if (m_myWindow)
1212  {
1213  m_myWindow->Close();
1214  m_myWindow = nullptr;
1215  }
1216 
1217  LOG(VB_PLAYBACK, LOG_INFO, LOC + "-- lock");
1218 
1219  // restore window to gui size and position
1220  if (m_mainWindow)
1221  {
1222  MythDisplay* display = m_mainWindow->GetDisplay();
1223  if (display->UsingVideoModes())
1224  {
1225  bool hide = display->NextModeIsLarger(display->GetGUIResolution());
1226  if (hide)
1227  m_mainWindow->hide();
1228  display->SwitchToGUI(true);
1229  if (hide)
1230  m_mainWindow->Show();
1231  }
1233  #ifdef Q_OS_ANDROID
1234  m_mainWindow->Show();
1235  #else
1236  m_mainWindow->show();
1237  #endif
1238  m_mainWindow->PauseIdleTimer(false);
1239  }
1240 
1241  qDeleteAll(m_screenPressKeyMapPlayback);
1243  qDeleteAll(m_screenPressKeyMapLiveTV);
1244  m_screenPressKeyMapLiveTV.clear();
1245 
1246  delete m_lastProgram;
1247 
1248  if (LCD *lcd = LCD::Get())
1249  {
1250  lcd->setFunctionLEDs(FUNC_TV, false);
1251  lcd->setFunctionLEDs(FUNC_MOVIE, false);
1252  lcd->switchToTime();
1253  }
1254 
1255  m_playerLock.lockForWrite();
1257  m_player = nullptr;
1258  m_playerLock.unlock();
1259 
1260  LOG(VB_PLAYBACK, LOG_DEBUG, LOC + "-- end");
1261 }
1262 
1267 {
1268  while (true)
1269  {
1270  QCoreApplication::processEvents();
1272  {
1273  m_wantsToQuit = true;
1274  return;
1275  }
1276 
1277  TVState state = GetState();
1278  if ((kState_Error == state) || (kState_None == state))
1279  return;
1280 
1281  if (kState_ChangingState == state)
1282  continue;
1283 
1285  m_playerContext.LockDeletePlayer(__FILE__, __LINE__);
1286  if (m_player && !m_player->IsErrored())
1287  {
1288  m_player->EventLoop();
1289  m_player->VideoLoop();
1290  }
1291  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
1292  ReturnPlayerLock();
1293 
1295  return;
1296  }
1297 }
1298 
1302 void TV::UpdateChannelList(int GroupID)
1303 {
1304  if (!m_dbUseChannelGroups)
1305  return;
1306 
1307  QMutexLocker locker(&m_channelGroupLock);
1308  if (GroupID == m_channelGroupId)
1309  return;
1310 
1311  ChannelInfoList list;
1312  if (GroupID >= 0)
1313  {
1314  list = ChannelUtil::GetChannels(0, true, "channum, callsign", static_cast<uint>(GroupID));
1315  ChannelUtil::SortChannels(list, "channum", true);
1316  }
1317 
1318  m_channelGroupId = GroupID;
1320 
1322  gCoreContext->SaveSetting("ChannelGroupDefault", m_channelGroupId);
1323 }
1324 
1326 {
1329  ret = m_playerContext.GetState();
1330  return ret;
1331 }
1332 
1333 // XXX what about subtitlezoom?
1335 {
1336  QVariantMap status;
1337 
1339  status.insert("state", StateToString(GetState()));
1340  m_playerContext.LockPlayingInfo(__FILE__, __LINE__);
1342  {
1343  status.insert("title", m_playerContext.m_playingInfo->GetTitle());
1344  status.insert("subtitle", m_playerContext.m_playingInfo->GetSubtitle());
1345  status.insert("starttime", m_playerContext.m_playingInfo->GetRecordingStartTime()
1346  .toUTC().toString("yyyy-MM-ddThh:mm:ssZ"));
1347  status.insert("chanid", QString::number(m_playerContext.m_playingInfo->GetChanID()));
1348  status.insert("programid", m_playerContext.m_playingInfo->GetProgramID());
1349  status.insert("pathname", m_playerContext.m_playingInfo->GetPathname());
1350  }
1351  m_playerContext.UnlockPlayingInfo(__FILE__, __LINE__);
1352  osdInfo info;
1354  m_playerContext.LockDeletePlayer(__FILE__, __LINE__);
1355  if (m_player)
1356  {
1357  if (!info.text["totalchapters"].isEmpty())
1358  {
1359  QList<std::chrono::seconds> chapters;
1360  m_player->GetChapterTimes(chapters);
1361  QVariantList var;
1362  for (std::chrono::seconds chapter : qAsConst(chapters))
1363  var << QVariant((long long)chapter.count());
1364  status.insert("chaptertimes", var);
1365  }
1366 
1368  QVariantMap tracks;
1369 
1370  QStringList list = m_player->GetTracks(kTrackTypeSubtitle);
1371  int currenttrack = -1;
1372  if (!list.isEmpty() && (kDisplayAVSubtitle == capmode))
1373  currenttrack = m_player->GetTrack(kTrackTypeSubtitle);
1374  for (int i = 0; i < list.size(); i++)
1375  {
1376  if (i == currenttrack)
1377  status.insert("currentsubtitletrack", list[i]);
1378  tracks.insert("SELECTSUBTITLE_" + QString::number(i), list[i]);
1379  }
1380 
1382  currenttrack = -1;
1383  if (!list.isEmpty() && (kDisplayTeletextCaptions == capmode))
1384  currenttrack = m_player->GetTrack(kTrackTypeTeletextCaptions);
1385  for (int i = 0; i < list.size(); i++)
1386  {
1387  if (i == currenttrack)
1388  status.insert("currentsubtitletrack", list[i]);
1389  tracks.insert("SELECTTTC_" + QString::number(i), list[i]);
1390  }
1391 
1393  currenttrack = -1;
1394  if (!list.isEmpty() && (kDisplayCC708 == capmode))
1395  currenttrack = m_player->GetTrack(kTrackTypeCC708);
1396  for (int i = 0; i < list.size(); i++)
1397  {
1398  if (i == currenttrack)
1399  status.insert("currentsubtitletrack", list[i]);
1400  tracks.insert("SELECTCC708_" + QString::number(i), list[i]);
1401  }
1402 
1404  currenttrack = -1;
1405  if (!list.isEmpty() && (kDisplayCC608 == capmode))
1406  currenttrack = m_player->GetTrack(kTrackTypeCC608);
1407  for (int i = 0; i < list.size(); i++)
1408  {
1409  if (i == currenttrack)
1410  status.insert("currentsubtitletrack", list[i]);
1411  tracks.insert("SELECTCC608_" + QString::number(i), list[i]);
1412  }
1413 
1415  currenttrack = -1;
1416  if (!list.isEmpty() && (kDisplayRawTextSubtitle == capmode))
1417  currenttrack = m_player->GetTrack(kTrackTypeRawText);
1418  for (int i = 0; i < list.size(); i++)
1419  {
1420  if (i == currenttrack)
1421  status.insert("currentsubtitletrack", list[i]);
1422  tracks.insert("SELECTRAWTEXT_" + QString::number(i), list[i]);
1423  }
1424 
1426  {
1427  if (kDisplayTextSubtitle == capmode)
1428  status.insert("currentsubtitletrack", tr("External Subtitles"));
1429  tracks.insert(ACTION_ENABLEEXTTEXT, tr("External Subtitles"));
1430  }
1431 
1432  status.insert("totalsubtitletracks", tracks.size());
1433  if (!tracks.isEmpty())
1434  status.insert("subtitletracks", tracks);
1435 
1436  tracks.clear();
1438  currenttrack = m_player->GetTrack(kTrackTypeAudio);
1439  for (int i = 0; i < list.size(); i++)
1440  {
1441  if (i == currenttrack)
1442  status.insert("currentaudiotrack", list[i]);
1443  tracks.insert("SELECTAUDIO_" + QString::number(i), list[i]);
1444  }
1445 
1446  status.insert("totalaudiotracks", tracks.size());
1447  if (!tracks.isEmpty())
1448  status.insert("audiotracks", tracks);
1449 
1450  status.insert("playspeed", m_player->GetPlaySpeed());
1451  status.insert("audiosyncoffset", static_cast<long long>(m_audioState.m_audioOffset.count()));
1452 
1454  {
1455  status.insert("volume", m_audioState.m_volume);
1456  status.insert("mute", m_audioState.m_muteState);
1457  }
1458 
1461  status.insert("brightness", m_videoColourState.GetValue(kPictureAttribute_Brightness));
1463  status.insert("contrast", m_videoColourState.GetValue(kPictureAttribute_Contrast));
1465  status.insert("colour", m_videoColourState.GetValue(kPictureAttribute_Colour));
1466  if (supp & kPictureAttributeSupported_Hue)
1467  status.insert("hue", m_videoColourState.GetValue(kPictureAttribute_Hue));
1468  }
1469  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
1470  ReturnPlayerLock();
1471 
1472  for (auto tit =info.text.cbegin(); tit != info.text.cend(); ++tit)
1473  status.insert(tit.key(), tit.value());
1474 
1475  QHashIterator<QString,int> vit(info.values);
1476  while (vit.hasNext())
1477  {
1478  vit.next();
1479  status.insert(vit.key(), vit.value());
1480  }
1481 
1483 }
1484 
1490 bool TV::LiveTV(bool ShowDialogs, const ChannelInfoList &Selection)
1491 {
1492  m_requestDelete = false;
1493  m_allowRerecord = false;
1494  m_jumpToProgram = false;
1495 
1497  if (m_playerContext.GetState() == kState_None && RequestNextRecorder(ShowDialogs, Selection))
1498  {
1501  m_switchToRec = nullptr;
1502 
1503  // Start Idle Timer
1504  if (m_dbIdleTimeout > 0ms)
1505  {
1507  LOG(VB_GENERAL, LOG_INFO, QString("Using Idle Timer. %1 minutes")
1508  .arg(duration_cast<std::chrono::minutes>(m_dbIdleTimeout).count()));
1509  }
1510 
1511  ReturnPlayerLock();
1512  return true;
1513  }
1514  ReturnPlayerLock();
1515  return false;
1516 }
1517 
1518 bool TV::RequestNextRecorder(bool ShowDialogs, const ChannelInfoList &Selection)
1519 {
1520  m_playerContext.SetRecorder(nullptr);
1521 
1522  RemoteEncoder *testrec = nullptr;
1523  if (m_switchToRec)
1524  {
1525  // If this is set we, already got a new recorder in SwitchCards()
1526  testrec = m_switchToRec;
1527  m_switchToRec = nullptr;
1528  }
1529  else if (!Selection.empty())
1530  {
1531  for (const auto & ci : Selection)
1532  {
1533  uint chanid = ci.m_chanId;
1534  QString channum = ci.m_chanNum;
1535  if (!chanid || channum.isEmpty())
1536  continue;
1537  QVector<uint> cards = IsTunableOn(&m_playerContext, chanid);
1538 
1539  if (chanid && !channum.isEmpty() && !cards.isEmpty())
1540  {
1541  testrec = RemoteGetExistingRecorder(static_cast<int>(*(cards.begin())));
1542  m_initialChanID = chanid;
1543  break;
1544  }
1545  }
1546  }
1547  else
1548  {
1549  // When starting LiveTV we just get the next free recorder
1550  testrec = RemoteRequestNextFreeRecorder(-1);
1551  }
1552 
1553  if (!testrec)
1554  return false;
1555 
1556  if (!testrec->IsValidRecorder())
1557  {
1558  if (ShowDialogs)
1560 
1561  delete testrec;
1562 
1563  return false;
1564  }
1565 
1566  m_playerContext.SetRecorder(testrec);
1567 
1568  return true;
1569 }
1570 
1571 void TV::AskAllowRecording(const QStringList &Msg, int Timeuntil, bool HasRec, bool HasLater)
1572 {
1573  if (!StateIsLiveTV(GetState()))
1574  return;
1575 
1576  auto *info = new ProgramInfo(Msg);
1577  if (!info->GetChanID())
1578  {
1579  delete info;
1580  return;
1581  }
1582 
1583  QMutexLocker locker(&m_askAllowLock);
1584  QString key = info->MakeUniqueKey();
1585  if (Timeuntil > 0)
1586  {
1587  // add program to list
1588 #if 0
1589  LOG(VB_GENERAL, LOG_DEBUG, LOC + "AskAllowRecording -- " +
1590  QString("adding '%1'").arg(info->m_title));
1591 #endif
1592  QDateTime expiry = MythDate::current().addSecs(Timeuntil);
1593  m_askAllowPrograms[key] = AskProgramInfo(expiry, HasRec, HasLater, info);
1594  }
1595  else
1596  {
1597  // remove program from list
1598  LOG(VB_GENERAL, LOG_INFO, LOC + "-- " +
1599  QString("removing '%1'").arg(info->GetTitle()));
1600  QMap<QString,AskProgramInfo>::iterator it = m_askAllowPrograms.find(key);
1601  if (it != m_askAllowPrograms.end())
1602  {
1603  delete (*it).m_info;
1604  m_askAllowPrograms.erase(it);
1605  }
1606  delete info;
1607  }
1608 
1609  ShowOSDAskAllow();
1610 }
1611 
1613 {
1614  QMutexLocker locker(&m_askAllowLock);
1616  return;
1617 
1618  uint cardid = m_playerContext.GetCardID();
1619 
1620  QString single_rec = tr("MythTV wants to record \"%1\" on %2 in %d seconds. Do you want to:");
1621 
1622  QString record_watch = tr("Record and watch while it records");
1623  QString let_record1 = tr("Let it record and go back to the Main Menu");
1624  QString let_recordm = tr("Let them record and go back to the Main Menu");
1625  QString record_later1 = tr("Record it later, I want to watch TV");
1626  QString record_laterm = tr("Record them later, I want to watch TV");
1627  QString do_not_record1= tr("Don't let it record, I want to watch TV");
1628  QString do_not_recordm= tr("Don't let them record, I want to watch TV");
1629 
1630  // eliminate timed out programs
1631  QDateTime timeNow = MythDate::current();
1632  QMap<QString,AskProgramInfo>::iterator it = m_askAllowPrograms.begin();
1633  while (it != m_askAllowPrograms.end())
1634  {
1635  if ((*it).m_expiry <= timeNow)
1636  {
1637 #if 0
1638  LOG(VB_GENERAL, LOG_DEBUG, LOC + "-- " +
1639  QString("removing '%1'").arg((*it).m_info->m_title));
1640 #endif
1641  delete (*it).m_info;
1642  it = m_askAllowPrograms.erase(it);
1643  }
1644  else
1645  {
1646  it++;
1647  }
1648  }
1649  std::chrono::milliseconds timeuntil = 0ms;
1650  QString message;
1651  uint conflict_count = static_cast<uint>(m_askAllowPrograms.size());
1652 
1653  it = m_askAllowPrograms.begin();
1654  if ((1 == m_askAllowPrograms.size()) && ((*it).m_info->GetInputID() == cardid))
1655  {
1656  (*it).m_isInSameInputGroup = (*it).m_isConflicting = true;
1657  }
1658  else if (!m_askAllowPrograms.empty())
1659  {
1660  // get the currently used input on our card
1661  bool busy_input_grps_loaded = false;
1662  std::vector<uint> busy_input_grps;
1663  InputInfo busy_input;
1664  RemoteIsBusy(cardid, busy_input);
1665 
1666  // check if current input can conflict
1667  for (it = m_askAllowPrograms.begin(); it != m_askAllowPrograms.end(); ++it)
1668  {
1669  (*it).m_isInSameInputGroup =
1670  (cardid == (*it).m_info->GetInputID());
1671 
1672  if ((*it).m_isInSameInputGroup)
1673  continue;
1674 
1675  // is busy_input in same input group as recording
1676  if (!busy_input_grps_loaded)
1677  {
1678  busy_input_grps = CardUtil::GetInputGroups(busy_input.m_inputId);
1679  busy_input_grps_loaded = true;
1680  }
1681 
1682  std::vector<uint> input_grps =
1683  CardUtil::GetInputGroups((*it).m_info->GetInputID());
1684 
1685  for (uint grp : input_grps)
1686  {
1687  if (find(busy_input_grps.begin(), busy_input_grps.end(),
1688  grp) != busy_input_grps.end())
1689  {
1690  (*it).m_isInSameInputGroup = true;
1691  break;
1692  }
1693  }
1694  }
1695 
1696  // check if inputs that can conflict are ok
1697  conflict_count = 0;
1698  for (it = m_askAllowPrograms.begin(); it != m_askAllowPrograms.end(); ++it)
1699  {
1700  if (!(*it).m_isInSameInputGroup)
1701  (*it).m_isConflicting = false; // NOLINT(bugprone-branch-clone)
1702  else if (cardid == (*it).m_info->GetInputID())
1703  (*it).m_isConflicting = true; // NOLINT(bugprone-branch-clone)
1704  else if (!CardUtil::IsTunerShared(cardid, (*it).m_info->GetInputID()))
1705  (*it).m_isConflicting = true;
1706  else if ((busy_input.m_mplexId &&
1707  (busy_input.m_mplexId == (*it).m_info->QueryMplexID())) ||
1708  (!busy_input.m_mplexId &&
1709  (busy_input.m_chanId == (*it).m_info->GetChanID())))
1710  (*it).m_isConflicting = false;
1711  else
1712  (*it).m_isConflicting = true;
1713 
1714  conflict_count += (*it).m_isConflicting ? 1 : 0;
1715  }
1716  }
1717 
1718  it = m_askAllowPrograms.begin();
1719  for (; it != m_askAllowPrograms.end() && !(*it).m_isConflicting; ++it);
1720 
1721  if (conflict_count == 0)
1722  {
1723  LOG(VB_GENERAL, LOG_INFO, LOC + "The scheduler wants to make "
1724  "a non-conflicting recording.");
1725  // TODO take down mplexid and inform user of problem
1726  // on channel changes.
1727  }
1728  else if (conflict_count == 1 && ((*it).m_info->GetInputID() == cardid))
1729  {
1730 #if 0
1731  LOG(VB_GENERAL, LOG_DEBUG, LOC + "UpdateOSDAskAllowDialog -- " +
1732  "kAskAllowOneRec");
1733 #endif
1734 
1735  it = m_askAllowPrograms.begin();
1736 
1737  QString channel = m_dbChannelFormat;
1738  channel
1739  .replace("<num>", (*it).m_info->GetChanNum())
1740  .replace("<sign>", (*it).m_info->GetChannelSchedulingID())
1741  .replace("<name>", (*it).m_info->GetChannelName());
1742 
1743  message = single_rec.arg((*it).m_info->GetTitle(), channel);
1744 
1745  BrowseEnd(false);
1746  timeuntil = MythDate::secsInFuture((*it).m_expiry);
1747  MythOSDDialogData dialog { OSD_DLG_ASKALLOW, message, timeuntil };
1748  dialog.m_buttons.push_back({ record_watch, "DIALOG_ASKALLOW_WATCH_0", false, !((*it).m_hasRec)} );
1749  dialog.m_buttons.push_back({ let_record1, "DIALOG_ASKALLOW_EXIT_0" });
1750  dialog.m_buttons.push_back({ ((*it).m_hasLater) ? record_later1 : do_not_record1,
1751  "DIALOG_ASKALLOW_CANCELRECORDING_0", false, ((*it).m_hasRec) });
1752  emit ChangeOSDDialog(dialog);
1753  }
1754  else
1755  {
1756  if (conflict_count > 1)
1757  {
1758  message = tr(
1759  "MythTV wants to record these programs in %d seconds:");
1760  message += "\n";
1761  }
1762 
1763  bool has_rec = false;
1764  for (it = m_askAllowPrograms.begin(); it != m_askAllowPrograms.end(); ++it)
1765  {
1766  if (!(*it).m_isConflicting)
1767  continue;
1768 
1769  QString title = (*it).m_info->GetTitle();
1770  if ((title.length() < 10) && !(*it).m_info->GetSubtitle().isEmpty())
1771  title += ": " + (*it).m_info->GetSubtitle();
1772  if (title.length() > 20)
1773  title = title.left(17) + "...";
1774 
1775  QString channel = m_dbChannelFormat;
1776  channel
1777  .replace("<num>", (*it).m_info->GetChanNum())
1778  .replace("<sign>", (*it).m_info->GetChannelSchedulingID())
1779  .replace("<name>", (*it).m_info->GetChannelName());
1780 
1781  if (conflict_count > 1)
1782  {
1783  message += tr("\"%1\" on %2").arg(title, channel);
1784  message += "\n";
1785  }
1786  else
1787  {
1788  message = single_rec.arg((*it).m_info->GetTitle(), channel);
1789  has_rec = (*it).m_hasRec;
1790  }
1791  }
1792 
1793  if (conflict_count > 1)
1794  {
1795  message += "\n";
1796  message += tr("Do you want to:");
1797  }
1798 
1799  bool all_have_later = true;
1800  timeuntil = 9999999ms;
1801  for (it = m_askAllowPrograms.begin(); it != m_askAllowPrograms.end(); ++it)
1802  {
1803  if ((*it).m_isConflicting)
1804  {
1805  all_have_later &= (*it).m_hasLater;
1806  auto tmp = std::chrono::milliseconds(MythDate::secsInFuture((*it).m_expiry));
1807  timeuntil = std::clamp(tmp, 0ms, timeuntil);
1808  }
1809  }
1810  timeuntil = (9999999ms == timeuntil) ? 0ms : timeuntil;
1811 
1812  if (conflict_count > 1)
1813  {
1814  BrowseEnd(false);
1815  emit ChangeOSDDialog( { OSD_DLG_ASKALLOW, message, timeuntil, {
1816  { let_recordm, "DIALOG_ASKALLOW_EXIT_0", false, true },
1817  { all_have_later ? record_laterm : do_not_recordm, "DIALOG_ASKALLOW_CANCELCONFLICTING_0" }
1818  }});
1819  }
1820  else
1821  {
1822  BrowseEnd(false);
1823  emit ChangeOSDDialog( {OSD_DLG_ASKALLOW, message, timeuntil, {
1824  { let_record1, "DIALOG_ASKALLOW_EXIT_0", false, !has_rec},
1825  { all_have_later ? record_later1 : do_not_record1, "DIALOG_ASKALLOW_CANCELRECORDING_0", false, has_rec}
1826  }});
1827  }
1828  }
1829 }
1830 
1831 void TV::HandleOSDAskAllow(const QString& Action)
1832 {
1834  return;
1835 
1836  if (!m_askAllowLock.tryLock())
1837  {
1838  LOG(VB_GENERAL, LOG_ERR, "allowrecordingbox : askAllowLock is locked");
1839  return;
1840  }
1841 
1842  if (Action == "CANCELRECORDING")
1843  {
1846  }
1847  else if (Action == "CANCELCONFLICTING")
1848  {
1849  for (const auto& pgm : qAsConst(m_askAllowPrograms))
1850  {
1851  if (pgm.m_isConflicting)
1852  RemoteCancelNextRecording(pgm.m_info->GetInputID(), true);
1853  }
1854  }
1855  else if (Action == "WATCH")
1856  {
1859  }
1860  else // if (action == "EXIT")
1861  {
1862  PrepareToExitPlayer(__LINE__);
1863  SetExitPlayer(true, true);
1864  }
1865 
1866  m_askAllowLock.unlock();
1867 }
1868 
1870 {
1871  m_wantsToQuit = false;
1872  m_jumpToProgram = false;
1873  m_allowRerecord = false;
1874  m_requestDelete = false;
1876 
1879  {
1880  ReturnPlayerLock();
1881  return 0;
1882  }
1883 
1887 
1888  ReturnPlayerLock();
1889 
1890  if (LCD *lcd = LCD::Get())
1891  {
1892  lcd->switchToChannel(ProgInfo.GetChannelSchedulingID(), ProgInfo.GetTitle(), ProgInfo.GetSubtitle());
1893  lcd->setFunctionLEDs((ProgInfo.IsRecording())?FUNC_TV:FUNC_MOVIE, true);
1894  }
1895 
1896  return 1;
1897 }
1898 
1900 {
1902 }
1903 
1905 {
1906  return (State == kState_WatchingPreRecorded ||
1911 }
1912 
1914 {
1915  return (State == kState_WatchingLiveTV);
1916 }
1917 
1918 #define TRANSITION(ASTATE,BSTATE) ((ctxState == (ASTATE)) && (desiredNextState == (BSTATE)))
1919 
1920 #define SET_NEXT() do { nextState = desiredNextState; changed = true; } while(false)
1921 #define SET_LAST() do { nextState = ctxState; changed = true; } while(false)
1922 
1923 static QString tv_i18n(const QString &msg)
1924 {
1925  QByteArray msg_arr = msg.toLatin1();
1926  QString msg_i18n = TV::tr(msg_arr.constData());
1927  QByteArray msg_i18n_arr = msg_i18n.toLatin1();
1928  return (msg_arr == msg_i18n_arr) ? msg_i18n : msg;
1929 }
1930 
1940 {
1941  if (m_playerContext.IsErrored())
1942  {
1943  LOG(VB_GENERAL, LOG_ERR, LOC + "Called after fatal error detected.");
1944  return;
1945  }
1946 
1947  bool changed = false;
1948 
1950  TVState nextState = m_playerContext.GetState();
1951  if (m_playerContext.m_nextState.empty())
1952  {
1953  LOG(VB_GENERAL, LOG_WARNING, LOC + "Warning, called with no state to change to.");
1955  return;
1956  }
1957 
1958  TVState ctxState = m_playerContext.GetState();
1959  TVState desiredNextState = m_playerContext.DequeueNextState();
1960 
1961  LOG(VB_GENERAL, LOG_INFO, LOC + QString("Attempting to change from %1 to %2")
1962  .arg(StateToString(nextState), StateToString(desiredNextState)));
1963 
1964  if (desiredNextState == kState_Error)
1965  {
1966  LOG(VB_GENERAL, LOG_ERR, LOC + "Attempting to set to an error state!");
1967  SetErrored();
1969  return;
1970  }
1971 
1972  bool ok = false;
1974  {
1976 
1978 
1979  QDateTime timerOffTime = MythDate::current();
1980  m_lockTimerOn = false;
1981 
1982  SET_NEXT();
1983 
1984  uint chanid = m_initialChanID;
1985  if (!chanid)
1986  chanid = static_cast<uint>(gCoreContext->GetNumSetting("DefaultChanid", 0));
1987 
1988  if (chanid && !IsTunablePriv(chanid))
1989  chanid = 0;
1990 
1991  QString channum = "";
1992 
1993  if (chanid)
1994  {
1995  QStringList reclist;
1996 
1997  MSqlQuery query(MSqlQuery::InitCon());
1998  query.prepare("SELECT channum FROM channel "
1999  "WHERE chanid = :CHANID");
2000  query.bindValue(":CHANID", chanid);
2001  if (query.exec() && query.isActive() && query.size() > 0 && query.next())
2002  channum = query.value(0).toString();
2003  else
2004  channum = QString::number(chanid);
2005 
2007  QString::number(chanid));
2008 
2009  if (getit)
2010  reclist = ChannelUtil::GetValidRecorderList(chanid, channum);
2011 
2012  if (!reclist.empty())
2013  {
2014  RemoteEncoder *testrec = RemoteRequestFreeRecorderFromList(reclist, 0);
2015  if (testrec && testrec->IsValidRecorder())
2016  {
2017  m_playerContext.SetRecorder(testrec);
2019  }
2020  else
2021  delete testrec; // If testrec isn't a valid recorder ...
2022  }
2023  else if (getit)
2024  chanid = 0;
2025  }
2026 
2027  LOG(VB_GENERAL, LOG_DEBUG, LOC + "Spawning LiveTV Recorder -- begin");
2028 
2029  if (chanid && !channum.isEmpty())
2031  else
2033 
2034  LOG(VB_GENERAL, LOG_DEBUG, LOC + "Spawning LiveTV Recorder -- end");
2035 
2037  {
2038  LOG(VB_GENERAL, LOG_ERR, LOC + "LiveTV not successfully started");
2040  m_playerContext.SetRecorder(nullptr);
2041  SetErrored();
2042  SET_LAST();
2043  }
2044  else
2045  {
2046  m_playerContext.LockPlayingInfo(__FILE__, __LINE__);
2047  QString playbackURL = m_playerContext.m_playingInfo->GetPlaybackURL(true);
2048  m_playerContext.UnlockPlayingInfo(__FILE__, __LINE__);
2049 
2050  bool opennow = (m_playerContext.m_tvchain->GetInputType(-1) != "DUMMY");
2051 
2052  LOG(VB_GENERAL, LOG_INFO, LOC +
2053  QString("playbackURL(%1) inputtype(%2)")
2054  .arg(playbackURL, m_playerContext.m_tvchain->GetInputType(-1)));
2055 
2058  playbackURL, false, true,
2059  opennow ? MythMediaBuffer::kLiveTVOpenTimeout : -1ms));
2060 
2063  }
2064 
2065 
2067  {
2068  ok = StartPlayer(desiredNextState);
2069  }
2070  if (!ok)
2071  {
2072  LOG(VB_GENERAL, LOG_ERR, LOC + "LiveTV not successfully started");
2074  m_playerContext.SetRecorder(nullptr);
2075  SetErrored();
2076  SET_LAST();
2077  }
2078  else
2079  {
2080  if (!m_lastLockSeenTime.isValid() ||
2081  (m_lastLockSeenTime < timerOffTime))
2082  {
2083  m_lockTimer.start();
2084  m_lockTimerOn = true;
2085  }
2086  }
2087  }
2089  {
2090  SET_NEXT();
2092  StopStuff(true, true, true);
2093  }
2099  {
2100  m_playerContext.LockPlayingInfo(__FILE__, __LINE__);
2101  QString playbackURL = m_playerContext.m_playingInfo->GetPlaybackURL(true);
2102  m_playerContext.UnlockPlayingInfo(__FILE__, __LINE__);
2103 
2104  MythMediaBuffer *buffer = MythMediaBuffer::Create(playbackURL, false);
2105  if (buffer && !buffer->GetLastError().isEmpty())
2106  {
2107  ShowNotificationError(tr("Can't start playback"),
2108  TV::tr( "TV Player" ), buffer->GetLastError());
2109  delete buffer;
2110  buffer = nullptr;
2111  }
2113 
2115  {
2116  if (desiredNextState == kState_WatchingRecording)
2117  {
2118  m_playerContext.LockPlayingInfo(__FILE__, __LINE__);
2120  m_playerContext.UnlockPlayingInfo(__FILE__, __LINE__);
2121 
2123 
2125  {
2126  LOG(VB_GENERAL, LOG_ERR, LOC +
2127  "Couldn't find recorder for in-progress recording");
2128  desiredNextState = kState_WatchingPreRecorded;
2129  m_playerContext.SetRecorder(nullptr);
2130  }
2131  else
2132  {
2134  }
2135  }
2136 
2137  ok = StartPlayer(desiredNextState);
2138 
2139  if (ok)
2140  {
2141  SET_NEXT();
2142 
2143  m_playerContext.LockPlayingInfo(__FILE__, __LINE__);
2145  {
2146  QString message = "COMMFLAG_REQUEST ";
2148  gCoreContext->SendMessage(message);
2149  }
2150  m_playerContext.UnlockPlayingInfo(__FILE__, __LINE__);
2151  }
2152  }
2153 
2154  if (!ok)
2155  {
2156  SET_LAST();
2157  SetErrored();
2159  {
2161  TV::tr( "TV Player" ),
2162  playbackURL);
2163  // We're going to display this error as notification
2164  // no need to display it later as popup
2166  }
2167  }
2168  }
2174  {
2175  SET_NEXT();
2177  StopStuff(true, true, false);
2178  }
2181  {
2182  SET_NEXT();
2183  }
2184 
2185  // Print state changed message...
2186  if (!changed)
2187  {
2188  LOG(VB_GENERAL, LOG_ERR, LOC + QString("Unknown state transition: %1 to %2")
2189  .arg(StateToString(m_playerContext.GetState()), StateToString(desiredNextState)));
2190  }
2191  else if (m_playerContext.GetState() != nextState)
2192  {
2193  LOG(VB_GENERAL, LOG_INFO, LOC + QString("Changing from %1 to %2")
2194  .arg(StateToString(m_playerContext.GetState()), StateToString(nextState)));
2195  }
2196 
2197  // update internal state variable
2198  TVState lastState = m_playerContext.GetState();
2199  m_playerContext.m_playingState = nextState;
2201 
2203  {
2204  LOG(VB_GENERAL, LOG_INFO, LOC + "State is LiveTV");
2205  UpdateOSDInput();
2206  LOG(VB_GENERAL, LOG_INFO, LOC + "UpdateOSDInput done");
2207  UpdateLCD();
2208  LOG(VB_GENERAL, LOG_INFO, LOC + "UpdateLCD done");
2209  ITVRestart(true);
2210  LOG(VB_GENERAL, LOG_INFO, LOC + "ITVRestart done");
2211  }
2212  else if (StateIsPlaying(m_playerContext.GetState()) && lastState == kState_None)
2213  {
2214  m_playerContext.LockPlayingInfo(__FILE__, __LINE__);
2215  int count = PlayGroup::GetCount();
2216  QString msg = tr("%1 Settings")
2218  m_playerContext.UnlockPlayingInfo(__FILE__, __LINE__);
2219  if (count > 0)
2220  emit ChangeOSDMessage(msg);
2221  ITVRestart(false);
2222  }
2223 
2225  {
2226  UpdateLCD();
2227  }
2228 
2231 
2237 
2241 
2243  {
2245  }
2246 
2253  {
2255  // m_playerBounds is not applicable when switching modes so
2256  // skip this logic in that case.
2257  if (!m_dbUseVideoModes)
2259 
2260  if (!m_weDisabledGUI)
2261  {
2262  m_weDisabledGUI = true;
2264  }
2265  // we no longer need the contents of myWindow
2266  if (m_myWindow)
2268 
2269  LOG(VB_GENERAL, LOG_INFO, LOC + "Main UI disabled.");
2270  }
2271 
2272  LOG(VB_PLAYBACK, LOG_DEBUG, LOC + " -- end");
2273 }
2274 
2275 #undef TRANSITION
2276 #undef SET_NEXT
2277 #undef SET_LAST
2278 
2284 bool TV::StartRecorder(std::chrono::milliseconds MaxWait)
2285 {
2287  MaxWait = (MaxWait <= 0ms) ? 40s : MaxWait;
2288  MythTimer t;
2289  t.start();
2290  bool recording = false;
2291  bool ok = true;
2292  if (!rec)
2293  {
2294  LOG(VB_GENERAL, LOG_ERR, LOC + "Invalid Remote Encoder");
2295  SetErrored();
2296  return false;
2297  }
2298  while (!(recording = rec->IsRecording(&ok)) && !m_exitPlayerTimerId && t.elapsed() < MaxWait)
2299  {
2300  if (!ok)
2301  {
2302  LOG(VB_GENERAL, LOG_ERR, LOC + "Lost contact with backend");
2303  SetErrored();
2304  return false;
2305  }
2306  std::this_thread::sleep_for(5us);
2307  }
2308 
2309  if (!recording || m_exitPlayerTimerId)
2310  {
2311  if (!m_exitPlayerTimerId)
2312  LOG(VB_GENERAL, LOG_ERR, LOC + "Timed out waiting for recorder to start");
2313  return false;
2314  }
2315 
2316  LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("Took %1 ms to start recorder.")
2317  .arg(t.elapsed().count()));
2318  return true;
2319 }
2320 
2334 void TV::StopStuff(bool StopRingBuffer, bool StopPlayer, bool StopRecorder)
2335 {
2336  LOG(VB_PLAYBACK, LOG_DEBUG, LOC + "-- begin");
2337 
2338  emit PlaybackExiting(this);
2339 
2342 
2343  m_playerContext.LockDeletePlayer(__FILE__, __LINE__);
2344  if (StopPlayer)
2346  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
2347 
2348  if (StopRingBuffer)
2349  {
2350  LOG(VB_PLAYBACK, LOG_INFO, LOC + "Stopping ring buffer");
2352  {
2356  }
2357  }
2358 
2359  if (StopRecorder)
2360  {
2361  LOG(VB_PLAYBACK, LOG_INFO, LOC + "stopping recorder");
2364  }
2365 
2366  LOG(VB_PLAYBACK, LOG_DEBUG, LOC + "-- end");
2367 }
2368 
2369 void TV::timerEvent(QTimerEvent *Event)
2370 {
2371  const int timer_id = Event->timerId();
2372 
2374  bool errored = m_playerContext.IsErrored();
2375  ReturnPlayerLock();
2376  if (errored)
2377  return;
2378 
2379  bool handled = true;
2380  if (timer_id == m_lcdTimerId)
2382  else if (timer_id == m_lcdVolumeTimerId)
2384  else if (timer_id == m_sleepTimerId)
2385  ShowOSDSleep();
2386  else if (timer_id == m_sleepDialogTimerId)
2388  else if (timer_id == m_idleTimerId)
2389  ShowOSDIdle();
2390  else if (timer_id == m_idleDialogTimerId)
2392  else if (timer_id == m_endOfPlaybackTimerId)
2394  else if (timer_id == m_endOfRecPromptTimerId)
2396  else if (timer_id == m_videoExitDialogTimerId)
2398  else if (timer_id == m_pseudoChangeChanTimerId)
2400  else if (timer_id == m_speedChangeTimerId)
2402  else if (timer_id == m_saveLastPlayPosTimerId)
2404  else
2405  handled = false;
2406 
2407  if (handled)
2408  return;
2409 
2410  // Check if it matches a signalMonitorTimerId
2411  if (timer_id == m_signalMonitorTimerId)
2412  {
2416  if (!m_playerContext.m_lastSignalMsg.empty())
2417  {
2418  // set last signal msg, so we get some feedback...
2421  }
2423  ReturnPlayerLock();
2424  return;
2425  }
2426 
2427  // Check if it matches networkControlTimerId
2428  QString netCmd;
2429  if (timer_id == m_networkControlTimerId)
2430  {
2431  if (!m_networkControlCommands.empty())
2432  netCmd = m_networkControlCommands.dequeue();
2433  if (m_networkControlCommands.empty())
2434  {
2437  }
2438  }
2439 
2440  if (!netCmd.isEmpty())
2441  {
2444  ReturnPlayerLock();
2445  handled = true;
2446  }
2447 
2448  if (handled)
2449  return;
2450 
2451  // Check if it matches exitPlayerTimerId
2452  if (timer_id == m_exitPlayerTimerId)
2453  {
2455  emit DialogQuit();
2456  emit HideAll();
2457 
2459  {
2460  if (!m_lastProgram->IsFileReadable())
2461  {
2462  emit ChangeOSDMessage(tr("Last Program: %1 Doesn't Exist").arg(m_lastProgram->GetTitle()));
2463  lastProgramStringList.clear();
2464  SetLastProgram(nullptr);
2465  LOG(VB_PLAYBACK, LOG_ERR, LOC + "Last Program File does not exist");
2466  m_jumpToProgram = false;
2467  }
2468  else
2469  {
2471  }
2472  }
2473  else
2475 
2476  ReturnPlayerLock();
2477 
2479  m_exitPlayerTimerId = 0;
2480  handled = true;
2481  }
2482 
2483  if (handled)
2484  return;
2485 
2486  if (timer_id == m_ccInputTimerId)
2487  {
2489  // Clear closed caption input mode when timer expires
2490  if (m_ccInputMode)
2491  {
2492  m_ccInputMode = false;
2493  ClearInputQueues(true);
2494  }
2495  ReturnPlayerLock();
2496 
2498  m_ccInputTimerId = 0;
2499  handled = true;
2500  }
2501 
2502  if (handled)
2503  return;
2504 
2505  if (timer_id == m_asInputTimerId)
2506  {
2508  // Clear closed caption input mode when timer expires
2509  if (m_asInputMode)
2510  {
2511  m_asInputMode = false;
2512  ClearInputQueues(true);
2513  }
2514  ReturnPlayerLock();
2515 
2517  m_asInputTimerId = 0;
2518  handled = true;
2519  }
2520 
2521  if (handled)
2522  return;
2523 
2524  if (timer_id == m_queueInputTimerId)
2525  {
2527  // Commit input when the OSD fades away
2528  if (HasQueuedChannel())
2529  {
2530  OSD *osd = GetOSDL();
2531  if (osd && !osd->IsWindowVisible(OSD_WIN_INPUT))
2532  {
2533  ReturnOSDLock();
2535  }
2536  else
2537  {
2538  ReturnOSDLock();
2539  }
2540  }
2541  ReturnPlayerLock();
2542 
2543  if (!m_queuedChanID && m_queuedChanNum.isEmpty() && m_queueInputTimerId)
2544  {
2546  m_queueInputTimerId = 0;
2547  }
2548  handled = true;
2549  }
2550 
2551  if (handled)
2552  return;
2553 
2554  if (timer_id == m_browseTimerId)
2555  {
2557  BrowseEnd(false);
2558  ReturnPlayerLock();
2559  handled = true;
2560  }
2561 
2562  if (handled)
2563  return;
2564 
2565  if (timer_id == m_errorRecoveryTimerId)
2566  {
2570  {
2571  SetExitPlayer(true, false);
2573  }
2574  ReturnPlayerLock();
2575 
2579  return;
2580  }
2581 
2582  LOG(VB_GENERAL, LOG_WARNING, LOC + QString("Unknown timer: %1").arg(timer_id));
2583 }
2584 
2586 {
2588  LCD *lcd = LCD::Get();
2589  if (lcd)
2590  {
2591  float progress = 0.0F;
2592  QString lcd_time_string;
2593  bool showProgress = true;
2594 
2595  if (StateIsLiveTV(GetState()))
2597 
2599  {
2600  ShowLCDDVDInfo();
2602  }
2603 
2604  if (showProgress)
2605  {
2606  osdInfo info;
2607  if (CalcPlayerSliderPosition(info)) {
2608  progress = info.values["position"] * 0.001F;
2609 
2610  lcd_time_string = info.text["playedtime"] + " / " + info.text["totaltime"];
2611  // if the string is longer than the LCD width, remove all spaces
2612  if (lcd_time_string.length() > static_cast<int>(lcd->getLCDWidth()))
2613  lcd_time_string.remove(' ');
2614  }
2615  }
2616  lcd->setChannelProgress(lcd_time_string, progress);
2617  }
2618  ReturnPlayerLock();
2619 
2621  m_lcdTimerId = StartTimer(kLCDTimeout, __LINE__);
2622 
2623  return true;
2624 }
2625 
2627 {
2629  LCD *lcd = LCD::Get();
2630  if (lcd)
2631  {
2634  }
2635  ReturnPlayerLock();
2636 
2638  m_lcdVolumeTimerId = 0;
2639 }
2640 
2641 int TV::StartTimer(std::chrono::milliseconds Interval, int Line)
2642 {
2643  int timer = startTimer(Interval);
2644  if (!timer)
2645  LOG(VB_GENERAL, LOG_ERR, LOC + QString("Failed to start timer on line %1 of %2").arg(Line).arg(__FILE__));
2646  return timer;
2647 }
2648 
2649 void TV::KillTimer(int Id)
2650 {
2651  killTimer(Id);
2652 }
2653 
2655 {
2658 }
2659 
2661 {
2662  auto StateChange = [&]()
2663  {
2665  if (!m_playerContext.m_nextState.empty())
2666  {
2668  if ((kState_None == m_playerContext.GetState() ||
2670  {
2671  ReturnPlayerLock();
2674  m_player = nullptr;
2675  }
2676  }
2677  ReturnPlayerLock();
2678  };
2679 
2680  QTimer::singleShot(0, this, StateChange);
2681 }
2682 
2684 {
2685  auto InputChange = [&]()
2686  {
2688  if (m_switchToInputId)
2689  {
2691  m_switchToInputId = 0;
2692  SwitchInputs(0, QString(), tmp);
2693  }
2694  ReturnPlayerLock();
2695  };
2696 
2697  QTimer::singleShot(0, this, InputChange);
2698 }
2699 
2701 {
2702  m_playerContext.m_errored = true;
2704  m_errorRecoveryTimerId = StartTimer(1ms, __LINE__);
2705 }
2706 
2708 {
2709  LOG(VB_GENERAL, LOG_INFO, LOC + QString("Switching to program: %1")
2710  .arg(ProgInfo.toString(ProgramInfo::kTitleSubtitle)));
2712  PrepareToExitPlayer(__LINE__);
2713  m_jumpToProgram = true;
2714  SetExitPlayer(true, true);
2715 }
2716 
2718 {
2719  m_playerContext.LockDeletePlayer(__FILE__, Line);
2721  {
2722  // Clear last play position when we're at the end of a recording.
2723  // unless the recording is in-progress.
2724  bool at_end = !StateIsRecording(m_playerContext.GetState()) &&
2726 
2727  // Clear/Save play position without notification
2728  // The change must be broadcast when file is no longer in use
2729  // to update previews, ie. with the MarkNotInUse notification
2730  uint64_t frame = at_end ? 0 : m_playerContext.m_player->GetFramesPlayed();
2732  emit UpdateLastPlayPosition(frame);
2733  if (m_dbAutoSetWatched)
2734  m_player->SetWatched();
2735  }
2736  m_playerContext.UnlockDeletePlayer(__FILE__, Line);
2737 }
2738 
2739 void TV::SetExitPlayer(bool SetIt, bool WantsTo)
2740 {
2741  if (SetIt)
2742  {
2743  m_wantsToQuit = WantsTo;
2744  if (!m_exitPlayerTimerId)
2745  m_exitPlayerTimerId = StartTimer(1ms, __LINE__);
2746  }
2747  else
2748  {
2749  if (m_exitPlayerTimerId)
2751  m_exitPlayerTimerId = 0;
2752  m_wantsToQuit = WantsTo;
2753  }
2754 }
2755 
2757 {
2761 
2762  bool is_playing = false;
2764  if (StateIsPlaying(GetState()))
2765  {
2767  {
2768  is_playing = true;
2769  }
2770  // If the end of playback is destined to pop up the end of
2771  // recording delete prompt, then don't exit the player here.
2772  else if (!(GetState() == kState_WatchingPreRecorded &&
2774  {
2776  m_endOfRecording = true;
2777  PrepareToExitPlayer(__LINE__);
2778  SetExitPlayer(true, true);
2779  }
2780  }
2781  ReturnPlayerLock();
2782 
2783  if (is_playing)
2785 }
2786 
2788 {
2791  {
2792  return;
2793  }
2794 
2796  OSD *osd = GetOSDL();
2797  if (osd && osd->DialogVisible())
2798  {
2799  ReturnOSDLock();
2800  ReturnPlayerLock();
2801  return;
2802  }
2803  ReturnOSDLock();
2804 
2805  m_playerContext.LockDeletePlayer(__FILE__, __LINE__);
2806  bool do_prompt = (m_playerContext.GetState() == kState_WatchingPreRecorded &&
2808  !m_player->IsPlaying());
2809  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
2810 
2811  if (do_prompt)
2812  ShowOSDPromptDeleteRecording(tr("End Of Recording"));
2813 
2814  ReturnPlayerLock();
2815 }
2816 
2818 {
2822 
2823  // disable dialog and exit playback after timeout
2825  OSD *osd = GetOSDL();
2826  if (!osd || !osd->DialogVisible(OSD_DLG_VIDEOEXIT))
2827  {
2828  ReturnOSDLock();
2829  ReturnPlayerLock();
2830  return;
2831  }
2832  ReturnOSDLock();
2833  DoTogglePause(true);
2834  ClearOSD();
2835  PrepareToExitPlayer(__LINE__);
2836  ReturnPlayerLock();
2837 
2838  m_requestDelete = false;
2839  SetExitPlayer(true, true);
2840 }
2841 
2843 {
2846 
2847  bool restartTimer = false;
2850  {
2852  {
2853  restartTimer = true;
2854  }
2855  else
2856  {
2857  LOG(VB_CHANNEL, LOG_INFO, "REC_PROGRAM -- channel change");
2858 
2860  QString channum = m_playerContext.m_pseudoLiveTVRec->GetChanNum();
2862 
2863  m_playerContext.m_prevChan.clear();
2864  ChangeChannel(chanid, channum);
2867  }
2868  }
2869  ReturnPlayerLock();
2870 
2871  if (restartTimer)
2873  m_pseudoChangeChanTimerId = StartTimer(25ms, __LINE__);
2874 }
2875 
2876 void TV::SetSpeedChangeTimer(std::chrono::milliseconds When, int Line)
2877 {
2880  m_speedChangeTimerId = StartTimer(When, Line);
2881 }
2882 
2884 {
2888 
2890  bool update_msg = m_playerContext.HandlePlayerSpeedChangeFFRew();
2892  if (update_msg)
2894  ReturnPlayerLock();
2895 }
2896 
2919 bool TV::eventFilter(QObject* Object, QEvent* Event)
2920 {
2921  // We want to intercept all resize events sent to the main window
2922  if ((Event->type() == QEvent::Resize))
2923  return (m_mainWindow != Object) ? false : event(Event);
2924 
2925  // Intercept keypress events unless they need to be handled by a main UI
2926  // screen (e.g. GuideGrid, ProgramFinder)
2927 
2928  if ( (QEvent::KeyPress == Event->type() || QEvent::KeyRelease == Event->type())
2929  && m_ignoreKeyPresses )
2930  return TVPlaybackState::eventFilter(Object, Event);
2931 
2932  QScopedPointer<QEvent> sNewEvent(nullptr);
2933  if (m_mainWindow->KeyLongPressFilter(&Event, sNewEvent))
2934  return true;
2935 
2936  if (QEvent::KeyPress == Event->type())
2937  return event(Event);
2938 
2939  if (MythGestureEvent::kEventType == Event->type())
2940  return m_ignoreKeyPresses ? false : event(Event);
2941 
2942  if (Event->type() == MythEvent::MythEventMessage ||
2943  Event->type() == MythEvent::MythUserMessage ||
2945  Event->type() == MythMediaEvent::kEventType)
2946  {
2947  // DO NOT call TV::customEvent here!
2948  // customEvent(Event);
2949  return true;
2950  }
2951 
2952  switch (Event->type())
2953  {
2954  case QEvent::Paint:
2955  case QEvent::UpdateRequest:
2956  case QEvent::Enter:
2957  {
2958  event(Event);
2959  return TVPlaybackState::eventFilter(Object, Event);
2960  }
2961  default:
2962  return TVPlaybackState::eventFilter(Object, Event);
2963  }
2964 }
2965 
2967 bool TV::event(QEvent* Event)
2968 {
2969  if (Event == nullptr)
2970  return TVPlaybackState::event(Event);
2971 
2972  if (QEvent::Resize == Event->type())
2973  {
2974  // These events probably aren't received by a direct call from
2975  // the Qt event dispacther, but are received by way of the event
2976  // dispatcher calling TV::eventFilter(MainWindow, Event).
2977  const auto *qre = dynamic_cast<const QResizeEvent*>(Event);
2978  if (qre)
2979  emit WindowResized(qre->size());
2980  return TVPlaybackState::event(Event);
2981  }
2982 
2983  if (QEvent::KeyPress == Event->type() || MythGestureEvent::kEventType == Event->type())
2984  {
2985  // These events aren't received by a direct call from the Qt
2986  // event dispacther, but are received by way of the event
2987  // dispatcher calling TV::eventFilter(MainWindow, Event).
2988 #if DEBUG_ACTIONS
2989  if (QEvent::KeyPress == Event->type())
2990  {
2991  const auto * ke = dynamic_cast<QKeyEvent*>(Event);
2992  if (ke)
2993  {
2994  LOG(VB_GENERAL, LOG_INFO, LOC + QString("keypress: %1 '%2'")
2995  .arg(ke->key()).arg(ke->text()));
2996  }
2997  }
2998  else
2999  {
3000  const auto * ge = dynamic_cast<MythGestureEvent*>(Event);
3001  if (ge)
3002  {
3003  LOG(VB_GENERAL, LOG_INFO, LOC + QString("mythgesture: g:%1 pos:%2,%3 b:%4")
3004  .arg(ge->GetGesture()).arg(ge->GetPosition().x())
3005  .arg(ge->GetPosition().y()).arg(ge->GetButton()));
3006  }
3007  }
3008 #endif
3009  bool handled = false;
3011  if (m_playerContext.HasPlayer())
3012  handled = ProcessKeypressOrGesture(Event);
3013  ReturnPlayerLock();
3014  if (handled)
3015  return true;
3016  }
3017 
3018  switch (Event->type())
3019  {
3020  case QEvent::Paint:
3021  case QEvent::UpdateRequest:
3022  case QEvent::Enter:
3023  // These events aren't received by a direct call from the Qt
3024  // event dispacther, but are received by way of the event
3025  // dispatcher calling TV::eventFilter(MainWindow, Event).
3026  return true;
3027  default:
3028  break;
3029  }
3030 
3031  return QObject::event(Event);
3032 }
3033 
3034 bool TV::HandleTrackAction(const QString &Action)
3035 {
3036  bool handled = true;
3037 
3040  else if (ACTION_ENABLEEXTTEXT == Action)
3042  else if (ACTION_DISABLEEXTTEXT == Action)
3044  else if (ACTION_ENABLEFORCEDSUBS == Action)
3045  emit ChangeAllowForcedSubtitles(true);
3046  else if (ACTION_DISABLEFORCEDSUBS == Action)
3047  emit ChangeAllowForcedSubtitles(false);
3048  else if (Action == ACTION_ENABLESUBS)
3049  emit SetCaptionsEnabled(true, true);
3050  else if (Action == ACTION_DISABLESUBS)
3051  emit SetCaptionsEnabled(false, true);
3053  {
3054  if (m_ccInputMode)
3055  {
3056  bool valid = false;
3057  int page = GetQueuedInputAsInt(&valid, 16);
3058  if (m_vbimode == VBIMode::PAL_TT && valid)
3059  emit SetTeletextPage(static_cast<uint>(page));
3060  else if (m_vbimode == VBIMode::NTSC_CC)
3061  emit SetTrack(kTrackTypeCC608, static_cast<uint>(std::max(std::min(page - 1, 1), 0)));
3062 
3063  ClearInputQueues(true);
3064 
3065  m_ccInputMode = false;
3066  if (m_ccInputTimerId)
3067  {
3069  m_ccInputTimerId = 0;
3070  }
3071  }
3073  {
3074  ClearInputQueues(false);
3075  AddKeyToInputQueue(0);
3076 
3077  m_ccInputMode = true;
3078  m_asInputMode = false;
3080  if (m_asInputTimerId)
3081  {
3083  m_asInputTimerId = 0;
3084  }
3085  }
3086  else
3087  {
3088  emit ToggleCaptions();
3089  }
3090  }
3091  else if (Action.startsWith("TOGGLE"))
3092  {
3093  int type = to_track_type(Action.mid(6));
3095  emit EnableTeletext();
3096  else if (type >= kTrackTypeSubtitle)
3097  emit ToggleCaptionsByType(static_cast<uint>(type));
3098  else
3099  handled = false;
3100  }
3101  else if (Action.startsWith("SELECT"))
3102  {
3103  int type = to_track_type(Action.mid(6));
3104  uint num = Action.section("_", -1).toUInt();
3105  if (type >= kTrackTypeAudio)
3106  emit SetTrack(static_cast<uint>(type), num);
3107  else
3108  handled = false;
3109  }
3110  else if (Action.startsWith("NEXT") || Action.startsWith("PREV"))
3111  {
3112  int dir = (Action.startsWith("NEXT")) ? +1 : -1;
3113  int type = to_track_type(Action.mid(4));
3114  if (type >= kTrackTypeAudio)
3115  emit ChangeTrack(static_cast<uint>(type), dir);
3116  else if (Action.endsWith("CC"))
3117  emit ChangeCaptionTrack(dir);
3118  else
3119  handled = false;
3120  }
3121  else
3122  handled = false;
3123  return handled;
3124 }
3125 
3126 // Make a special check for global system-related events.
3127 //
3128 // This check needs to be done early in the keypress event processing,
3129 // because FF/REW processing causes unknown events to stop FF/REW, and
3130 // manual zoom mode processing consumes all but a few event types.
3131 // Ideally, we would just call MythScreenType::keyPressEvent()
3132 // unconditionally, but we only want certain keypresses handled by
3133 // that method.
3134 //
3135 // As a result, some of the MythScreenType::keyPressEvent() string
3136 // compare logic is copied here.
3137 static bool SysEventHandleAction(MythMainWindow* MainWindow, QKeyEvent *e, const QStringList &actions)
3138 {
3139  QStringList::const_iterator it;
3140  for (it = actions.begin(); it != actions.end(); ++it)
3141  {
3142  if ((*it).startsWith("SYSEVENT") ||
3143  *it == ACTION_SCREENSHOT ||
3144  *it == ACTION_TVPOWERON ||
3145  *it == ACTION_TVPOWEROFF)
3146  {
3147  return MainWindow->GetMainStack()->GetTopScreen()->keyPressEvent(e);
3148  }
3149  }
3150  return false;
3151 }
3152 
3153 QList<QKeyEvent*> TV::ConvertScreenPressKeyMap(const QString &KeyList)
3154 {
3155  QList<QKeyEvent*> keyPressList;
3156  int i = 0;
3157  QStringList stringKeyList = KeyList.split(',');
3158  for (const auto & str : qAsConst(stringKeyList))
3159  {
3160  QKeySequence keySequence(str);
3161  for (i = 0; i < keySequence.count(); i++)
3162  {
3163 #if QT_VERSION < QT_VERSION_CHECK(6,0,0)
3164  int keynum = keySequence[i];
3165  int keyCode = keynum & ~Qt::KeyboardModifierMask;
3166  auto modifiers = static_cast<Qt::KeyboardModifiers>(keynum & Qt::KeyboardModifierMask);
3167 #else
3168  int keyCode = keySequence[i].key();
3169  Qt::KeyboardModifiers modifiers = keySequence[i].keyboardModifiers();
3170 #endif
3171  auto * keyEvent = new QKeyEvent(QEvent::None, keyCode, modifiers);
3172  keyPressList.append(keyEvent);
3173  }
3174  }
3175  if (stringKeyList.count() < kScreenPressRegionCount)
3176  {
3177  // add default remainders
3178  for(; i < kScreenPressRegionCount; i++)
3179  {
3180  auto * keyEvent = new QKeyEvent(QEvent::None, Qt::Key_Escape, Qt::NoModifier);
3181  keyPressList.append(keyEvent);
3182  }
3183  }
3184  return keyPressList;
3185 }
3186 
3187 bool TV::TranslateGesture(const QString &Context, MythGestureEvent *Event,
3188  QStringList &Actions, bool IsLiveTV)
3189 {
3190  if (Event && Context == "TV Playback")
3191  {
3192  // TODO make this configuable via a similar mechanism to
3193  // TranslateKeyPress
3194  // possibly with configurable hot zones of various sizes in a theme
3195  // TODO enhance gestures to support other non Click types too
3196  if ((Event->GetGesture() == MythGestureEvent::Click) &&
3197  (Event->GetButton() == Qt::LeftButton))
3198  {
3199  // divide screen into 12 regions
3200  QSize size = m_mainWindow->size();
3201  QPoint pos = Event->GetPosition();
3202  int region = 0;
3203  const int widthDivider = 4;
3204  int w4 = size.width() / widthDivider;
3205  region = pos.x() / w4;
3206  int h3 = size.height() / 3;
3207  region += (pos.y() / h3) * widthDivider;
3208 
3209  if (IsLiveTV)
3210  return m_mainWindow->TranslateKeyPress(Context, m_screenPressKeyMapLiveTV[region], Actions, true);
3211  return m_mainWindow->TranslateKeyPress(Context, m_screenPressKeyMapPlayback[region], Actions, true);
3212  }
3213  return false;
3214  }
3215  return false;
3216 }
3217 
3218 bool TV::TranslateKeyPressOrGesture(const QString &Context, QEvent *Event,
3219  QStringList &Actions, bool IsLiveTV, bool AllowJumps)
3220 {
3221  if (Event)
3222  {
3223  if (QEvent::KeyPress == Event->type())
3224  return m_mainWindow->TranslateKeyPress(Context, dynamic_cast<QKeyEvent*>(Event), Actions, AllowJumps);
3225  if (MythGestureEvent::kEventType == Event->type())
3226  return TranslateGesture(Context, dynamic_cast<MythGestureEvent*>(Event), Actions, IsLiveTV);
3227  }
3228  return false;
3229 }
3230 
3232 {
3233  if (Event == nullptr)
3234  return false;
3235 
3236  bool ignoreKeys = m_playerContext.IsPlayerChangingBuffers();
3237 
3238 #if DEBUG_ACTIONS
3239  LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("ignoreKeys: %1").arg(ignoreKeys));
3240 #endif
3241 
3242  if (m_idleTimerId)
3243  {
3246  }
3247 
3248 #ifdef Q_OS_LINUX
3249  // Fixups for _some_ linux native codes that QT doesn't know
3250  auto* eKeyEvent = dynamic_cast<QKeyEvent*>(Event);
3251  if (eKeyEvent) {
3252  if (eKeyEvent->key() <= 0)
3253  {
3254  int keycode = 0;
3255  switch(eKeyEvent->nativeScanCode())
3256  {
3257  case 209: // XF86AudioPause
3258  keycode = Qt::Key_MediaPause;
3259  break;
3260  default:
3261  break;
3262  }
3263 
3264  if (keycode > 0)
3265  {
3266  auto *key = new QKeyEvent(QEvent::KeyPress, keycode, eKeyEvent->modifiers());
3267  QCoreApplication::postEvent(this, key);
3268  }
3269  }
3270  }
3271 #endif
3272 
3273  QStringList actions;
3274  bool handled = false;
3275  bool alreadyTranslatedPlayback = false;
3276 
3277  TVState state = GetState();
3278  bool isLiveTV = StateIsLiveTV(state);
3279 
3280  if (ignoreKeys)
3281  {
3282  handled = TranslateKeyPressOrGesture("TV Playback", Event, actions, isLiveTV);
3283  alreadyTranslatedPlayback = true;
3284 
3285  if (handled || actions.isEmpty())
3286  return handled;
3287 
3288  bool esc = IsActionable({ "ESCAPE", "BACK" }, actions);
3289  bool pause = IsActionable(ACTION_PAUSE, actions);
3290  bool play = IsActionable(ACTION_PLAY, actions);
3291 
3292  if ((!esc || m_overlayState.m_browsing) && !pause && !play)
3293  return false;
3294  }
3295 
3296  OSD *osd = GetOSDL();
3297  if (osd && osd->DialogVisible())
3298  {
3299  if (QEvent::KeyPress == Event->type())
3300  {
3301  auto *qke = dynamic_cast<QKeyEvent*>(Event);
3302  handled = (qke != nullptr) && osd->DialogHandleKeypress(qke);
3303  }
3304  if (MythGestureEvent::kEventType == Event->type())
3305  {
3306  auto *mge = dynamic_cast<MythGestureEvent*>(Event);
3307  handled = (mge != nullptr) && osd->DialogHandleGesture(mge);
3308  }
3309  }
3310  ReturnOSDLock();
3311 
3312  if (m_overlayState.m_editing && !handled)
3313  {
3314  handled |= TranslateKeyPressOrGesture("TV Editing", Event, actions, isLiveTV);
3315 
3316  if (!handled && m_player)
3317  {
3318  if (IsActionable("MENU", actions))
3319  {
3320  ShowOSDCutpoint("EDIT_CUT_POINTS");
3321  handled = true;
3322  }
3323  if (IsActionable(ACTION_MENUCOMPACT, actions))
3324  {
3325  ShowOSDCutpoint("EDIT_CUT_POINTS_COMPACT");
3326  handled = true;
3327  }
3328  if (IsActionable("ESCAPE", actions))
3329  {
3330  emit RefreshEditorState(true);
3331  if (!m_editorState.m_saved)
3332  ShowOSDCutpoint("EXIT_EDIT_MODE");
3333  else
3334  emit DisableEdit(0);
3335  handled = true;
3336  }
3337  else
3338  {
3339  emit RefreshEditorState();
3342  {
3343  ShowOSDCutpoint("EDIT_CUT_POINTS");
3344  handled = true;
3345  }
3346  else
3347  {
3348  handled |= m_player->HandleProgramEditorActions(actions);
3349  }
3350  }
3351  }
3352  }
3353 
3354  if (handled)
3355  return true;
3356 
3357  // If text is already queued up, be more lax on what is ok.
3358  // This allows hex teletext entry and minor channel entry.
3359  if (QEvent::KeyPress == Event->type())
3360  {
3361  auto *qke = dynamic_cast<QKeyEvent*>(Event);
3362  if (qke == nullptr)
3363  return false;
3364  const QString txt = qke->text();
3365  if (HasQueuedInput() && (1 == txt.length()))
3366  {
3367  bool ok = false;
3368  (void)txt.toInt(&ok, 16);
3369  if (ok || txt=="_" || txt=="-" || txt=="#" || txt==".")
3370  {
3371  AddKeyToInputQueue(txt.at(0).toLatin1());
3372  return true;
3373  }
3374  }
3375  }
3376 
3377  // Teletext menu
3379  {
3380  QStringList tt_actions;
3381  handled = TranslateKeyPressOrGesture("Teletext Menu", Event, tt_actions, isLiveTV);
3382 
3383  if (!handled && !tt_actions.isEmpty())
3384  {
3385  for (const QString& action : qAsConst(tt_actions))
3386  {
3387  emit HandleTeletextAction(action, handled);
3388  if (handled)
3389  return true;
3390  }
3391  }
3392  }
3393 
3394  // Interactive television
3396  {
3397  if (!alreadyTranslatedPlayback)
3398  {
3399  handled = TranslateKeyPressOrGesture("TV Playback", Event, actions, isLiveTV);
3400  alreadyTranslatedPlayback = true;
3401  }
3402 
3403  if (!handled && !actions.isEmpty())
3404  {
3405  for (const QString& action : qAsConst(actions))
3406  {
3407  emit HandleITVAction(action, handled);
3408  if (handled)
3409  return true;
3410  }
3411  }
3412  }
3413 
3414  if (!alreadyTranslatedPlayback)
3415  handled = TranslateKeyPressOrGesture("TV Playback", Event, actions, isLiveTV);
3416 
3417  if (handled || actions.isEmpty())
3418  return handled;
3419 
3420  handled = false;
3421 
3424 
3425  if (QEvent::KeyPress == Event->type())
3426  handled = handled || SysEventHandleAction(m_mainWindow, dynamic_cast<QKeyEvent*>(Event), actions);
3427  handled = handled || BrowseHandleAction(actions);
3428  handled = handled || ManualZoomHandleAction(actions);
3429  handled = handled || PictureAttributeHandleAction(actions);
3430  handled = handled || TimeStretchHandleAction(actions);
3431  handled = handled || AudioSyncHandleAction(actions);
3432  handled = handled || SubtitleZoomHandleAction(actions);
3433  handled = handled || SubtitleDelayHandleAction(actions);
3434  handled = handled || DiscMenuHandleAction(actions);
3435  handled = handled || ActiveHandleAction(actions, isDVD, isMenuOrStill);
3436  handled = handled || ToggleHandleAction(actions, isDVD);
3437  handled = handled || FFRewHandleAction(actions);
3438  handled = handled || ActivePostQHandleAction(actions);
3439 
3440 #if DEBUG_ACTIONS
3441  for (int i = 0; i < actions.size(); ++i)
3442  LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("handled(%1) actions[%2](%3)")
3443  .arg(handled).arg(i).arg(actions[i]));
3444 #endif // DEBUG_ACTIONS
3445 
3446  if (handled)
3447  return true;
3448 
3449  if (!handled)
3450  {
3451  for (int i = 0; i < actions.size() && !handled; i++)
3452  {
3453  QString action = actions[i];
3454  bool ok = false;
3455  int val = action.toInt(&ok);
3456 
3457  if (ok)
3458  {
3459  AddKeyToInputQueue(static_cast<char>('0' + val));
3460  handled = true;
3461  }
3462  }
3463  }
3464 
3465  return true;
3466 }
3467 
3468 bool TV::BrowseHandleAction(const QStringList &Actions)
3469 {
3471  return false;
3472 
3473  bool handled = true;
3474 
3475  if (IsActionable({ ACTION_UP, ACTION_CHANNELUP }, Actions))
3477  else if (IsActionable( { ACTION_DOWN, ACTION_CHANNELDOWN }, Actions))
3479  else if (IsActionable(ACTION_LEFT, Actions))
3481  else if (IsActionable(ACTION_RIGHT, Actions))
3483  else if (IsActionable("NEXTFAV", Actions))
3485  else if (IsActionable(ACTION_SELECT, Actions))
3486  BrowseEnd(true);
3487  else if (IsActionable({ ACTION_CLEAROSD, "ESCAPE", "BACK", "TOGGLEBROWSE" }, Actions))
3488  BrowseEnd(false);
3489  else if (IsActionable(ACTION_TOGGLERECORD, Actions))
3490  QuickRecord();
3491  else
3492  {
3493  handled = false;
3494  for (const auto& action : qAsConst(Actions))
3495  {
3496  if (action.length() == 1 && action[0].isDigit())
3497  {
3498  AddKeyToInputQueue(action[0].toLatin1());
3499  handled = true;
3500  }
3501  }
3502  }
3503 
3504  // only pass-through actions listed below
3505  static const QStringList passthrough =
3506  {
3507  ACTION_VOLUMEUP, ACTION_VOLUMEDOWN, "STRETCHINC", "STRETCHDEC",
3508  ACTION_MUTEAUDIO, "CYCLEAUDIOCHAN", "BOTTOMLINEMOVE", "BOTTOMLINESAVE", "TOGGLEASPECT"
3509  };
3510  return handled || !IsActionable(passthrough, Actions);
3511 }
3512 
3513 bool TV::ManualZoomHandleAction(const QStringList &Actions)
3514 {
3515  if (!m_zoomMode)
3516  return false;
3517 
3518  bool endmanualzoom = false;
3519  bool handled = true;
3520  bool updateOSD = true;
3521  ZoomDirection zoom = kZoom_END;
3523  zoom = kZoomUp;
3525  zoom = kZoomDown;
3526  else if (IsActionable({ ACTION_ZOOMLEFT, ACTION_LEFT }, Actions))
3527  zoom = kZoomLeft;
3528  else if (IsActionable({ ACTION_ZOOMRIGHT, ACTION_RIGHT }, Actions))
3529  zoom = kZoomRight;
3530  else if (IsActionable({ ACTION_ZOOMASPECTUP, ACTION_VOLUMEUP }, Actions))
3531  zoom = kZoomAspectUp;
3532  else if (IsActionable({ ACTION_ZOOMASPECTDOWN, ACTION_VOLUMEDOWN }, Actions))
3533  zoom = kZoomAspectDown;
3534  else if (IsActionable({ ACTION_ZOOMIN, ACTION_JUMPFFWD }, Actions))
3535  zoom = kZoomIn;
3536  else if (IsActionable({ ACTION_ZOOMOUT, ACTION_JUMPRWND }, Actions))
3537  zoom = kZoomOut;
3538  else if (IsActionable(ACTION_ZOOMVERTICALIN, Actions))
3539  zoom = kZoomVerticalIn;
3540  else if (IsActionable(ACTION_ZOOMVERTICALOUT, Actions))
3541  zoom = kZoomVerticalOut;
3542  else if (IsActionable(ACTION_ZOOMHORIZONTALIN, Actions))
3543  zoom = kZoomHorizontalIn;
3544  else if (IsActionable(ACTION_ZOOMHORIZONTALOUT, Actions))
3545  zoom = kZoomHorizontalOut;
3546  else if (IsActionable({ ACTION_ZOOMQUIT, "ESCAPE", "BACK" }, Actions))
3547  {
3548  zoom = kZoomHome;
3549  endmanualzoom = true;
3550  }
3551  else if (IsActionable({ ACTION_ZOOMCOMMIT, ACTION_SELECT }, Actions))
3552  {
3553  endmanualzoom = true;
3554  SetManualZoom(false, tr("Zoom Committed"));
3555  }
3556  else
3557  {
3558  updateOSD = false;
3559  // only pass-through actions listed below
3560  static const QStringList passthrough =
3561  {
3562  "STRETCHINC", "STRETCHDEC", ACTION_MUTEAUDIO,
3563  "CYCLEAUDIOCHAN", ACTION_PAUSE, ACTION_CLEAROSD
3564  };
3565  handled = !IsActionable(passthrough, Actions);
3566  }
3567 
3568  QString msg = tr("Zoom Committed");
3569  if (zoom != kZoom_END)
3570  {
3571  emit ChangeZoom(zoom);
3572  msg = endmanualzoom ? tr("Zoom Ignored") :
3576  }
3577  else if (endmanualzoom)
3578  {
3579  msg = tr("%1 Committed").arg(GetZoomString(m_videoBoundsState.m_manualHorizScale,
3582  }
3583 
3584  if (updateOSD)
3585  SetManualZoom(!endmanualzoom, msg);
3586 
3587  return handled;
3588 }
3589 
3590 bool TV::PictureAttributeHandleAction(const QStringList &Actions)
3591 {
3592  if (!m_adjustingPicture)
3593  return false;
3594 
3595  bool up = IsActionable(ACTION_RIGHT, Actions);
3596  bool down = up ? false : IsActionable(ACTION_LEFT, Actions);
3597  if (!(up || down))
3598  return false;
3599 
3601  {
3603  VolumeChange(up);
3604  else
3606  return true;
3607  }
3608 
3609  int value = 99;
3613  UpdateOSDStatus(toTitleString(m_adjustingPicture), text, QString::number(value),
3615  emit ChangeOSDPositionUpdates(false);
3616  return true;
3617 }
3618 
3619 bool TV::TimeStretchHandleAction(const QStringList &Actions)
3620 {
3621  if (!m_stretchAdjustment)
3622  return false;
3623 
3624  bool handled = true;
3625 
3626  if (IsActionable(ACTION_LEFT, Actions))
3627  ChangeTimeStretch(-1);
3628  else if (IsActionable(ACTION_RIGHT, Actions))
3629  ChangeTimeStretch(1);
3630  else if (IsActionable(ACTION_DOWN, Actions))
3631  ChangeTimeStretch(-5);
3632  else if (IsActionable(ACTION_UP, Actions))
3633  ChangeTimeStretch(5);
3634  else if (IsActionable("ADJUSTSTRETCH", Actions))
3636  else if (IsActionable(ACTION_SELECT, Actions))
3637  ClearOSD();
3638  else
3639  handled = false;
3640 
3641  return handled;
3642 }
3643 
3644 bool TV::AudioSyncHandleAction(const QStringList& Actions)
3645 {
3646  if (!m_audiosyncAdjustment)
3647  return false;
3648 
3649  bool handled = true;
3650 
3651  if (IsActionable(ACTION_LEFT, Actions))
3652  emit ChangeAudioOffset(-1ms);
3653  else if (IsActionable(ACTION_RIGHT, Actions))
3654  emit ChangeAudioOffset(1ms);
3655  else if (IsActionable(ACTION_UP, Actions))
3656  emit ChangeAudioOffset(10ms);
3657  else if (IsActionable(ACTION_DOWN, Actions))
3658  emit ChangeAudioOffset(-10ms);
3659  else if (IsActionable({ ACTION_TOGGELAUDIOSYNC, ACTION_SELECT }, Actions))
3660  ClearOSD();
3661  else
3662  handled = false;
3663 
3664  return handled;
3665 }
3666 
3667 bool TV::SubtitleZoomHandleAction(const QStringList &Actions)
3668 {
3670  return false;
3671 
3672  bool handled = true;
3673 
3674  if (IsActionable(ACTION_LEFT, Actions))
3675  emit AdjustSubtitleZoom(-1);
3676  else if (IsActionable(ACTION_RIGHT, Actions))
3677  emit AdjustSubtitleZoom(1);
3678  else if (IsActionable(ACTION_UP, Actions))
3679  emit AdjustSubtitleZoom(10);
3680  else if (IsActionable(ACTION_DOWN, Actions))
3681  emit AdjustSubtitleZoom(-10);
3682  else if (IsActionable({ ACTION_TOGGLESUBTITLEZOOM, ACTION_SELECT }, Actions))
3683  ClearOSD();
3684  else
3685  handled = false;
3686 
3687  return handled;
3688 }
3689 
3690 bool TV::SubtitleDelayHandleAction(const QStringList &Actions)
3691 {
3693  return false;
3694 
3695  bool handled = true;
3696 
3697  if (IsActionable(ACTION_LEFT, Actions))
3698  emit AdjustSubtitleDelay(-5ms);
3699  else if (IsActionable(ACTION_RIGHT, Actions))
3700  emit AdjustSubtitleDelay(5ms);
3701  else if (IsActionable(ACTION_UP, Actions))
3702  emit AdjustSubtitleDelay(25ms);
3703  else if (IsActionable(ACTION_DOWN, Actions))
3704  emit AdjustSubtitleDelay(-25ms);
3705  else if (IsActionable({ ACTION_TOGGLESUBTITLEDELAY, ACTION_SELECT }, Actions))
3706  ClearOSD();
3707  else
3708  handled = false;
3709 
3710  return handled;
3711 }
3712 
3713 bool TV::DiscMenuHandleAction(const QStringList& Actions) const
3714 {
3715  mpeg::chrono::pts pts = 0_pts;
3717  if (output)
3718  {
3719  MythVideoFrame *frame = output->GetLastShownFrame();
3720  // convert timecode (msec) to pts (90kHz)
3721  if (frame)
3722  pts = duration_cast<mpeg::chrono::pts>(frame->m_timecode);
3723  }
3725  return m_playerContext.m_buffer->HandleAction(Actions, pts);
3726  return false;
3727 }
3728 
3729 bool TV::ActiveHandleAction(const QStringList &Actions,
3730  bool IsDVD, bool IsDVDStillFrame)
3731 {
3732  bool handled = true;
3733 
3734  if (IsActionable("SKIPCOMMERCIAL", Actions) && !IsDVD)
3735  DoSkipCommercials(1);
3736  else if (IsActionable("SKIPCOMMBACK", Actions) && !IsDVD)
3737  DoSkipCommercials(-1);
3738  else if (IsActionable("QUEUETRANSCODE", Actions) && !IsDVD)
3739  DoQueueTranscode("Default");
3740  else if (IsActionable("QUEUETRANSCODE_AUTO", Actions) && !IsDVD)
3741  DoQueueTranscode("Autodetect");
3742  else if (IsActionable("QUEUETRANSCODE_HIGH", Actions) && !IsDVD)
3743  DoQueueTranscode("High Quality");
3744  else if (IsActionable("QUEUETRANSCODE_MEDIUM", Actions) && !IsDVD)
3745  DoQueueTranscode("Medium Quality");
3746  else if (IsActionable("QUEUETRANSCODE_LOW", Actions) && !IsDVD)
3747  DoQueueTranscode("Low Quality");
3748  else if (IsActionable(ACTION_PLAY, Actions))
3749  DoPlay();
3750  else if (IsActionable(ACTION_PAUSE, Actions))
3751  DoTogglePause(true);
3752  else if (IsActionable("SPEEDINC", Actions) && !IsDVDStillFrame)
3753  ChangeSpeed(1);
3754  else if (IsActionable("SPEEDDEC", Actions) && !IsDVDStillFrame)
3755  ChangeSpeed(-1);
3756  else if (IsActionable("ADJUSTSTRETCH", Actions))
3757  ChangeTimeStretch(0); // just display
3758  else if (IsActionable("CYCLECOMMSKIPMODE",Actions) && !IsDVD)
3760  else if (IsActionable("NEXTSCAN", Actions))
3761  {
3762  m_playerContext.LockDeletePlayer(__FILE__, __LINE__);
3764  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
3765  OverrideScan(scan);
3766  }
3767  else if (IsActionable(ACTION_SEEKARB, Actions) && !IsDVD)
3768  {
3769  if (m_asInputMode)
3770  {
3771  ClearInputQueues(true);
3772  emit ChangeOSDText(OSD_WIN_INPUT, {{"osd_number_entry", tr("Seek:")}}, kOSDTimeout_Med);
3773  m_asInputMode = false;
3774  if (m_asInputTimerId)
3775  {
3777  m_asInputTimerId = 0;
3778  }
3779  }
3780  else
3781  {
3782  ClearInputQueues(false);
3783  AddKeyToInputQueue(0);
3784  m_asInputMode = true;
3785  m_ccInputMode = false;
3787  if (m_ccInputTimerId)
3788  {
3790  m_ccInputTimerId = 0;
3791  }
3792  }
3793  }
3794  else if (IsActionable(ACTION_JUMPRWND, Actions))
3795  DoJumpRWND();
3796  else if (IsActionable(ACTION_JUMPFFWD, Actions))
3797  DoJumpFFWD();
3798  else if (IsActionable(ACTION_JUMPBKMRK, Actions))
3799  {
3800  m_playerContext.LockDeletePlayer(__FILE__, __LINE__);
3801  uint64_t bookmark = m_player->GetBookmark();
3802  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
3803 
3804  if (bookmark)
3805  {
3806  DoPlayerSeekToFrame(bookmark);
3807  m_playerContext.LockDeletePlayer(__FILE__, __LINE__);
3808  UpdateOSDSeekMessage(tr("Jump to Bookmark"), kOSDTimeout_Med);
3809  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
3810  }
3811  }
3812  else if (IsActionable(ACTION_JUMPSTART,Actions))
3813  {
3814  DoSeek(0, tr("Jump to Beginning"), /*timeIsOffset*/false, /*honorCutlist*/true);
3815  }
3816  else if (IsActionable(ACTION_CLEAROSD, Actions))
3817  {
3818  ClearOSD();
3819  }
3820  else if (IsActionable(ACTION_VIEWSCHEDULED, Actions))
3822  else if (HandleJumpToProgramAction(Actions))
3823  { // NOLINT(bugprone-branch-clone)
3824  }
3825  else if (IsActionable(ACTION_SIGNALMON, Actions))
3826  {
3828  {
3829  QString input = m_playerContext.m_recorder->GetInput();
3831 
3832  if (timeout == 0xffffffff)
3833  {
3834  emit ChangeOSDMessage("No Signal Monitor");
3835  return false;
3836  }
3837 
3838  std::chrono::milliseconds rate = m_sigMonMode ? 0ms : 100ms;
3839  bool notify = !m_sigMonMode;
3840 
3841  PauseLiveTV();
3843  UnpauseLiveTV();
3844 
3845  m_lockTimerOn = false;
3847  }
3848  }
3849  else if (IsActionable(ACTION_SCREENSHOT, Actions))
3850  {
3852  }
3853  else if (IsActionable(ACTION_STOP, Actions))
3854  {
3855  PrepareToExitPlayer(__LINE__);
3856  SetExitPlayer(true, true);
3857  }
3858  else if (IsActionable(ACTION_EXITSHOWNOPROMPTS, Actions))
3859  {
3860  m_requestDelete = false;
3861  PrepareToExitPlayer(__LINE__);
3862  SetExitPlayer(true, true);
3863  }
3864  else if (IsActionable({ "ESCAPE", "BACK" }, Actions))
3865  {
3868  {
3869  ClearOSD();
3870  }
3871  else
3872  {
3873  bool visible = false;
3874  emit IsOSDVisible(visible);
3875  if (visible)
3876  {
3877  ClearOSD();
3878  return handled;
3879  }
3880  }
3881 
3882  NormalSpeed();
3883  StopFFRew();
3884  bool exit = false;
3885  if (StateIsLiveTV(GetState()))
3886  {
3888  {
3890  return handled;
3891  }
3892  exit = true;
3893  }
3894  else
3895  {
3897  !m_underNetworkControl && !IsDVDStillFrame)
3898  {
3900  return handled;
3901  }
3902  PrepareToExitPlayer(__LINE__);
3903  m_requestDelete = false;
3904  exit = true;
3905  }
3906 
3907  if (exit)
3908  {
3909  // If it's a DVD, and we're not trying to execute a
3910  // jumppoint, try to back up.
3911  if (IsDVD && !m_mainWindow->IsExitingToMain() && IsActionable("BACK", Actions) &&
3913  {
3914  return handled;
3915  }
3916  SetExitPlayer(true, true);
3917  }
3918  }
3919  else if (IsActionable(ACTION_ENABLEUPMIX, Actions))
3920  emit ChangeUpmix(true);
3921  else if (IsActionable(ACTION_DISABLEUPMIX, Actions))
3922  emit ChangeUpmix(false);
3923  else if (IsActionable(ACTION_VOLUMEDOWN, Actions))
3924  VolumeChange(false);
3925  else if (IsActionable(ACTION_VOLUMEUP, Actions))
3926  VolumeChange(true);
3927  else if (IsActionable("CYCLEAUDIOCHAN", Actions))
3928  emit ChangeMuteState(true);
3929  else if (IsActionable(ACTION_MUTEAUDIO, Actions))
3930  emit ChangeMuteState();
3931  else if (IsActionable("STRETCHINC", Actions))
3932  ChangeTimeStretch(1);
3933  else if (IsActionable("STRETCHDEC", Actions))
3934  ChangeTimeStretch(-1);
3935  else if (IsActionable("MENU", Actions))
3936  ShowOSDMenu();
3937  else if (IsActionable(ACTION_MENUCOMPACT, Actions))
3938  ShowOSDMenu(true);
3939  else if (IsActionable({ "INFO", "INFOWITHCUTLIST" }, Actions))
3940  {
3941  if (HasQueuedInput())
3942  DoArbSeek(ARBSEEK_SET, IsActionable("INFOWITHCUTLIST", Actions));
3943  else
3944  ToggleOSD(true);
3945  }
3946  else if (IsActionable(ACTION_TOGGLEOSDDEBUG, Actions))
3947  emit ChangeOSDDebug();
3948  else if (!IsDVDStillFrame && SeekHandleAction(Actions, IsDVD))
3949  {
3950  }
3951  else
3952  {
3953  handled = false;
3954  for (auto it = Actions.cbegin(); it != Actions.cend() && !handled; ++it)
3955  handled = HandleTrackAction(*it);
3956  }
3957 
3958  return handled;
3959 }
3960 
3961 bool TV::FFRewHandleAction(const QStringList &Actions)
3962 {
3963  bool handled = false;
3964 
3966  {
3967  for (int i = 0; i < Actions.size() && !handled; i++)
3968  {
3969  const QString& action = Actions[i];
3970  bool ok = false;
3971  int val = action.toInt(&ok);
3972 
3973  if (ok && val < static_cast<int>(m_ffRewSpeeds.size()))
3974  {
3975  SetFFRew(val);
3976  handled = true;
3977  }
3978  }
3979 
3980  if (!handled)
3981  {
3984  handled = true;
3985  }
3986  }
3987 
3989  {
3990  NormalSpeed();
3992  handled = true;
3993  }
3994 
3995  return handled;
3996 }
3997 
3998 bool TV::ToggleHandleAction(const QStringList &Actions, bool IsDVD)
3999 {
4000  bool handled = true;
4001  bool islivetv = StateIsLiveTV(GetState());
4002 
4003  if (IsActionable(ACTION_BOTTOMLINEMOVE, Actions))
4004  emit ToggleMoveBottomLine();
4005  else if (IsActionable(ACTION_BOTTOMLINESAVE, Actions))
4006  emit SaveBottomLine();
4007  else if (IsActionable("TOGGLEASPECT", Actions))
4008  emit ChangeAspectOverride();
4009  else if (IsActionable("TOGGLEFILL", Actions))
4010  emit ChangeAdjustFill();
4011  else if (IsActionable(ACTION_TOGGELAUDIOSYNC, Actions))
4012  emit ChangeAudioOffset(0ms); // just display
4013  else if (IsActionable(ACTION_TOGGLESUBTITLEZOOM, Actions))
4014  emit AdjustSubtitleZoom(0); // just display
4015  else if (IsActionable(ACTION_TOGGLESUBTITLEDELAY, Actions))
4016  emit AdjustSubtitleDelay(0ms); // just display
4017  else if (IsActionable(ACTION_TOGGLEVISUALISATION, Actions))
4018  emit EnableVisualiser(false, true);
4019  else if (IsActionable(ACTION_ENABLEVISUALISATION, Actions))
4020  emit EnableVisualiser(true);
4021  else if (IsActionable(ACTION_DISABLEVISUALISATION, Actions))
4022  emit EnableVisualiser(false);
4023  else if (IsActionable("TOGGLEPICCONTROLS", Actions))
4025  else if (IsActionable("TOGGLESTRETCH", Actions))
4027  else if (IsActionable(ACTION_TOGGLEUPMIX, Actions))
4028  emit ChangeUpmix(false, true);
4029  else if (IsActionable(ACTION_TOGGLESLEEP, Actions))
4030  ToggleSleepTimer();
4031  else if (IsActionable(ACTION_TOGGLERECORD, Actions) && islivetv)
4032  QuickRecord();
4033  else if (IsActionable(ACTION_TOGGLEFAV, Actions) && islivetv)
4035  else if (IsActionable(ACTION_TOGGLECHANCONTROLS, Actions) && islivetv)
4037  else if (IsActionable(ACTION_TOGGLERECCONTROLS, Actions) && islivetv)
4039  else if (IsActionable("TOGGLEBROWSE", Actions))
4040  {
4041  if (islivetv)
4042  BrowseStart();
4043  else if (!IsDVD)
4044  ShowOSDMenu();
4045  else
4046  handled = false;
4047  }
4048  else if (IsActionable("EDIT", Actions))
4049  {
4050  if (islivetv)
4052  else if (!IsDVD)
4054  }
4055  else if (IsActionable(ACTION_OSDNAVIGATION, Actions))
4056  {
4058  }
4059  else
4060  {
4061  handled = false;
4062  }
4063 
4064  return handled;
4065 }
4066 
4068 {
4069  if (Clear)
4070  {
4071  emit UpdateBookmark(true);
4072  emit ChangeOSDMessage(tr("Bookmark Cleared"));
4073  }
4074  else // if (IsBookmarkAllowed(ctx))
4075  {
4076  emit UpdateBookmark();
4077  osdInfo info;
4079  info.text["title"] = tr("Position");
4081  emit ChangeOSDMessage(tr("Bookmark Saved"));
4082  }
4083 }
4084 
4085 bool TV::ActivePostQHandleAction(const QStringList &Actions)
4086 {
4087  bool handled = true;
4088  TVState state = GetState();
4089  bool islivetv = StateIsLiveTV(state);
4090  bool isdvd = state == kState_WatchingDVD;
4091  bool isdisc = isdvd || state == kState_WatchingBD;
4092 
4093  if (IsActionable(ACTION_SETBOOKMARK, Actions))
4094  {
4095  if (!CommitQueuedInput())
4096  {
4097  m_playerContext.LockDeletePlayer(__FILE__, __LINE__);
4098  SetBookmark(false);
4099  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
4100  }
4101  }
4102  if (IsActionable(ACTION_TOGGLEBOOKMARK, Actions))
4103  {
4104  if (!CommitQueuedInput())
4105  {
4106  m_playerContext.LockDeletePlayer(__FILE__, __LINE__);
4107  SetBookmark(m_player->GetBookmark() != 0U);
4108  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
4109  }
4110  }
4111  else if (IsActionable("NEXTFAV", Actions) && islivetv)
4113  else if (IsActionable("NEXTSOURCE", Actions) && islivetv)
4115  else if (IsActionable("PREVSOURCE", Actions) && islivetv)
4117  else if (IsActionable("NEXTINPUT", Actions) && islivetv)
4118  SwitchInputs();
4119  else if (IsActionable(ACTION_GUIDE, Actions))
4121  else if (IsActionable("PREVCHAN", Actions) && islivetv)
4122  PopPreviousChannel(false);
4123  else if (IsActionable(ACTION_CHANNELUP, Actions))
4124  {
4125  if (islivetv)
4126  {
4127  if (m_dbBrowseAlways)
4129  else
4131  }
4132  else
4133  DoJumpRWND();
4134  }
4135  else if (IsActionable(ACTION_CHANNELDOWN, Actions))
4136  {
4137  if (islivetv)
4138  {
4139  if (m_dbBrowseAlways)
4141  else
4143  }
4144  else
4145  DoJumpFFWD();
4146  }
4147  else if (IsActionable("DELETE", Actions) && !islivetv)
4148  {
4149  NormalSpeed();
4150  StopFFRew();
4151  PrepareToExitPlayer(__LINE__);
4152  ShowOSDPromptDeleteRecording(tr("Are you sure you want to delete:"));
4153  }
4154  else if (IsActionable(ACTION_JUMPTODVDROOTMENU, Actions) && isdisc)
4155  emit GoToMenu("root");
4156  else if (IsActionable(ACTION_JUMPTODVDCHAPTERMENU, Actions) && isdisc)
4157  emit GoToMenu("chapter");
4158  else if (IsActionable(ACTION_JUMPTODVDTITLEMENU, Actions) && isdisc)
4159  emit GoToMenu("title");
4160  else if (IsActionable(ACTION_JUMPTOPOPUPMENU, Actions) && isdisc)
4161  emit GoToMenu("popup");
4162  else if (IsActionable(ACTION_FINDER, Actions))
4164  else
4165  handled = false;
4166 
4167  return handled;
4168 }
4169 
4170 
4172 {
4173  bool ignoreKeys = m_playerContext.IsPlayerChangingBuffers();
4174 
4175 #ifdef DEBUG_ACTIONS
4176  LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("(%1) ignoreKeys: %2").arg(Command).arg(ignoreKeys));
4177 #endif
4178 
4179  if (ignoreKeys)
4180  {
4181  LOG(VB_GENERAL, LOG_WARNING, LOC + "Ignoring network control command because ignoreKeys is set");
4182  return;
4183  }
4184 
4185 #if QT_VERSION < QT_VERSION_CHECK(5,14,0)
4186  QStringList tokens = Command.split(" ", QString::SkipEmptyParts);
4187 #else
4188  QStringList tokens = Command.split(" ", Qt::SkipEmptyParts);
4189 #endif
4190  if (tokens.size() < 2)
4191  {
4192  LOG(VB_GENERAL, LOG_ERR, LOC + "Not enough tokens in network control command " + QString("'%1'").arg(Command));
4193  return;
4194  }
4195 
4196  OSD *osd = GetOSDL();
4197  bool dlg = false;
4198  if (osd)
4199  dlg = osd->DialogVisible();
4200  ReturnOSDLock();
4201 
4202  if (dlg)
4203  {
4204  LOG(VB_GENERAL, LOG_WARNING, LOC +
4205  "Ignoring network control command\n\t\t\t" +
4206  QString("because dialog is waiting for a response"));
4207  return;
4208  }
4209 
4210  if (tokens[1] != "QUERY")
4211  ClearOSD();
4212 
4213  if (tokens.size() == 3 && tokens[1] == "CHANID")
4214  {
4215  m_queuedChanID = tokens[2].toUInt();
4216  m_queuedChanNum.clear();
4218  }
4219  else if (tokens.size() == 3 && tokens[1] == "CHANNEL")
4220  {
4221  if (StateIsLiveTV(GetState()))
4222  {
4223  if (tokens[2] == "UP")
4225  else if (tokens[2] == "DOWN")
4227  else if (tokens[2].contains(QRegularExpression(R"(^[-\.\d_#]+$)")))
4228  ChangeChannel(0, tokens[2]);
4229  }
4230  }
4231  else if (tokens.size() == 3 && tokens[1] == "SPEED")
4232  {
4233  bool paused = ContextIsPaused(__FILE__, __LINE__);
4234 
4235  if (tokens[2] == "0x")
4236  {
4237  NormalSpeed();
4238  StopFFRew();
4239  if (!paused)
4240  DoTogglePause(true);
4241  }
4242  else if (tokens[2] == "normal")
4243  {
4244  NormalSpeed();
4245  StopFFRew();
4246  if (paused)
4247  DoTogglePause(true);
4248  return;
4249  }
4250  else
4251  {
4252  float tmpSpeed = 1.0F;
4253  bool ok = false;
4254 
4255  if (tokens[2].contains(QRegularExpression(R"(^\-*(\d*\.)?\d+x$)")))
4256  {
4257  QString speed = tokens[2].left(tokens[2].length()-1);
4258  tmpSpeed = speed.toFloat(&ok);
4259  }
4260  else
4261  {
4262  QRegularExpression re { R"(^(\-*\d+)\/(\d+)x$)" };
4263  auto match = re.match(tokens[2]);
4264  if (match.hasMatch())
4265  {
4266  QStringList matches = match.capturedTexts();
4267  int numerator = matches[1].toInt(&ok);
4268  int denominator = matches[2].toInt(&ok);
4269 
4270  if (ok && denominator != 0)
4271  tmpSpeed = static_cast<float>(numerator) / static_cast<float>(denominator);
4272  else
4273  ok = false;
4274  }
4275  }
4276 
4277  if (ok)
4278  {
4279  float searchSpeed = fabs(tmpSpeed);
4280 
4281  if (paused)
4282  DoTogglePause(true);
4283 
4284  if (tmpSpeed == 0.0F)
4285  {
4286  NormalSpeed();
4287  StopFFRew();
4288 
4289  if (!paused)
4290  DoTogglePause(true);
4291  }
4292  else if (tmpSpeed == 1.0F)
4293  {
4294  StopFFRew();
4295  m_playerContext.m_tsNormal = 1.0F;
4296  ChangeTimeStretch(0, false);
4297  return;
4298  }
4299 
4300  NormalSpeed();
4301 
4302  size_t index = 0;
4303  for ( ; index < m_ffRewSpeeds.size(); index++)
4304  if (m_ffRewSpeeds[index] == static_cast<int>(searchSpeed))
4305  break;
4306 
4307  if ((index < m_ffRewSpeeds.size()) && (m_ffRewSpeeds[index] == static_cast<int>(searchSpeed)))
4308  {
4309  if (tmpSpeed < 0)
4311  else if (tmpSpeed > 1)
4313  else
4314  StopFFRew();
4315 
4317  SetFFRew(static_cast<int>(index));
4318  }
4319  else if (0.48F <= tmpSpeed && tmpSpeed <= 2.0F)
4320  {
4321  StopFFRew();
4322  m_playerContext.m_tsNormal = tmpSpeed; // alter speed before display
4323  ChangeTimeStretch(0, false);
4324  }
4325  else
4326  {
4327  LOG(VB_GENERAL, LOG_WARNING, QString("Couldn't find %1 speed. Setting Speed to 1x")
4328  .arg(static_cast<double>(searchSpeed)));
4331  }
4332  }
4333  else
4334  {
4335  LOG(VB_GENERAL, LOG_ERR, QString("Found an unknown speed of %1").arg(tokens[2]));
4336  }
4337  }
4338  }
4339  else if (tokens.size() == 2 && tokens[1] == "STOP")
4340  {
4341  PrepareToExitPlayer(__LINE__);
4342  SetExitPlayer(true, true);
4343  }
4344  else if (tokens.size() >= 3 && tokens[1] == "SEEK" && m_playerContext.HasPlayer())
4345  {
4347  return;
4348 
4349  if (tokens[2] == "BEGINNING")
4350  {
4351  DoSeek(0, tr("Jump to Beginning"), /*timeIsOffset*/false, /*honorCutlist*/true);
4352  }
4353  else if (tokens[2] == "FORWARD")
4354  {
4355  DoSeek(m_playerContext.m_fftime, tr("Skip Ahead"), /*timeIsOffset*/true, /*honorCutlist*/true);
4356  }
4357  else if (tokens[2] == "BACKWARD")
4358  {
4359  DoSeek(-m_playerContext.m_rewtime, tr("Skip Back"), /*timeIsOffset*/true, /*honorCutlist*/true);
4360  }
4361  else if ((tokens[2] == "POSITION" ||
4362  tokens[2] == "POSITIONWITHCUTLIST") &&
4363  (tokens.size() == 4) &&
4364  (tokens[3].contains(QRegularExpression("^\\d+$"))))
4365  {
4366  DoSeekAbsolute(tokens[3].toInt(), tokens[2] == "POSITIONWITHCUTLIST");
4367  }
4368  }
4369  else if (tokens.size() >= 3 && tokens[1] == "SUBTITLES")
4370  {
4371  bool ok = false;
4372  uint track = tokens[2].toUInt(&ok);
4373 
4374  if (!ok)
4375  return;
4376 
4377  if (track == 0)
4378  {
4379  emit SetCaptionsEnabled(false, true);
4380  }
4381  else
4382  {
4383  QStringList subs = m_player->GetTracks(kTrackTypeSubtitle);
4384  uint size = static_cast<uint>(subs.size());
4385  uint start = 1;
4386  uint finish = start + size;
4387  if (track >= start && track < finish)
4388  {
4389  emit SetTrack(kTrackTypeSubtitle, track - start);
4391  return;
4392  }
4393 
4394  start = finish + 1;
4396  finish = start + size;
4397  if (track >= start && track < finish)
4398  {
4399  emit SetTrack(kTrackTypeCC708, track - start);
4401  return;
4402  }
4403 
4404  start = finish + 1;
4406  finish = start + size;
4407  if (track >= start && track < finish)
4408  {
4409  emit SetTrack(kTrackTypeCC608, track - start);
4411  return;
4412  }
4413 
4414  start = finish + 1;
4416  finish = start + size;
4417  if (track >= start && track < finish)
4418  {
4419  emit SetTrack(kTrackTypeTeletextCaptions, track - start);
4421  return;
4422  }
4423 
4424  start = finish + 1;
4426  finish = start + size;
4427  if (track >= start && track < finish)
4428  {
4429  emit SetTrack(kTrackTypeTeletextMenu, track - start);
4431  return;
4432  }
4433 
4434  start = finish + 1;
4436  finish = start + size;
4437  if (track >= start && track < finish)
4438  {
4439  emit SetTrack(kTrackTypeRawText, track - start);
4441  return;
4442  }
4443  }
4444  }
4445  else if (tokens.size() >= 3 && tokens[1] == "VOLUME")
4446  {
4447  QRegularExpression re { "(\\d+)%?" };
4448  auto match = re.match(tokens[2]);
4449  if (match.hasMatch())
4450  {
4451  QStringList matches = match.capturedTexts();
4452 
4453  LOG(VB_GENERAL, LOG_INFO, QString("Set Volume to %1%").arg(matches[1]));
4454 
4455  bool ok = false;
4456  int vol = matches[1].toInt(&ok);
4457  if (!ok)
4458  return;
4459 
4460  if (0 <= vol && vol <= 100)
4461  emit ChangeVolume(true, vol);
4462  }
4463  }
4464  else if (tokens.size() >= 3 && tokens[1] == "QUERY")
4465  {
4466  if (tokens[2] == "POSITION")
4467  {
4468  if (!m_player)
4469  return;
4470  QString speedStr;
4471  if (ContextIsPaused(__FILE__, __LINE__))
4472  {
4473  speedStr = "pause";
4474  }
4475  else if (m_playerContext.m_ffRewState)
4476  {
4477  speedStr = QString("%1x").arg(m_playerContext.m_ffRewSpeed);
4478  }
4479  else
4480  {
4481  QRegularExpression re { "Play (.*)x" };
4482  auto match = re.match(m_playerContext.GetPlayMessage());
4483  if (match.hasMatch())
4484  {
4485  QStringList matches = match.capturedTexts();
4486  speedStr = QString("%1x").arg(matches[1]);
4487  }
4488  else
4489  {
4490  speedStr = "1x";
4491  }
4492  }
4493 
4494  osdInfo info;
4495  CalcPlayerSliderPosition(info, true);
4496 
4497  QDateTime respDate = MythDate::current(true);
4498  QString infoStr = "";
4499 
4500  m_playerContext.LockDeletePlayer(__FILE__, __LINE__);
4501  uint64_t fplay = 0;
4502  double rate = 30.0;
4503  if (m_player)
4504  {
4505  fplay = m_player->GetFramesPlayed();
4506  rate = static_cast<double>(m_player->GetFrameRate()); // for display only
4507  }
4508  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
4509 
4510  m_playerContext.LockPlayingInfo(__FILE__, __LINE__);
4512  {
4513  infoStr = "LiveTV";
4516  }
4517  else
4518  {
4520  infoStr = "DVD";
4522  infoStr = "Recorded";
4523  else
4524  infoStr = "Video";
4525 
4528  }
4529 
4530  QString bufferFilename =
4531  m_playerContext.m_buffer ? m_playerContext.m_buffer->GetFilename() : QString("no buffer");
4532  if ((infoStr == "Recorded") || (infoStr == "LiveTV"))
4533  {
4534  infoStr += QString(" %1 %2 %3 %4 %5 %6 %7")
4535  .arg(info.text["description"],
4536  speedStr,
4537  m_playerContext.m_playingInfo != nullptr
4538  ? QString::number(m_playerContext.m_playingInfo->GetChanID()) : "0",
4539  respDate.toString(Qt::ISODate),
4540  QString::number(fplay),
4541  bufferFilename,
4542  QString::number(rate));
4543  }
4544  else
4545  {
4546  QString position = info.text["description"].section(" ",0,0);
4547  infoStr += QString(" %1 %2 %3 %4 %5")
4548  .arg(position,
4549  speedStr,
4550  bufferFilename,
4551  QString::number(fplay),
4552  QString::number(rate));
4553  }
4554 
4555  infoStr += QString(" Subtitles:");
4556 
4558 
4559  if (subtype == kDisplayNone)
4560  infoStr += QString(" *0:[None]*");
4561  else
4562  infoStr += QString(" 0:[None]");
4563 
4564  uint n = 1;
4565 
4566  QStringList subs = m_player->GetTracks(kTrackTypeSubtitle);
4567  for (int i = 0; i < subs.size(); i++)
4568  {
4569  if ((subtype & kDisplayAVSubtitle) && (m_player->GetTrack(kTrackTypeSubtitle) == i))
4570  infoStr += QString(" *%1:[%2]*").arg(n).arg(subs[i]);
4571  else
4572  infoStr += QString(" %1:[%2]").arg(n).arg(subs[i]);
4573  n++;
4574  }
4575 
4577  for (int i = 0; i < subs.size(); i++)
4578  {
4579  if ((subtype & kDisplayCC708) && (m_player->GetTrack(kTrackTypeCC708) == i))
4580  infoStr += QString(" *%1:[%2]*").arg(n).arg(subs[i]);
4581  else
4582  infoStr += QString(" %1:[%2]").arg(n).arg(subs[i]);
4583  n++;
4584  }
4585 
4587  for (int i = 0; i < subs.size(); i++)
4588  {
4589  if ((subtype & kDisplayCC608) && (m_player->GetTrack(kTrackTypeCC608) == i))
4590  infoStr += QString(" *%1:[%2]*").arg(n).arg(subs[i]);
4591  else
4592  infoStr += QString(" %1:[%2]").arg(n).arg(subs[i]);
4593  n++;
4594  }
4595 
4597  for (int i = 0; i < subs.size(); i++)
4598  {
4600  infoStr += QString(" *%1:[%2]*").arg(n).arg(subs[i]);
4601  else
4602  infoStr += QString(" %1:[%2]").arg(n).arg(subs[i]);
4603  n++;
4604  }
4605 
4607  for (int i = 0; i < subs.size(); i++)
4608  {
4610  infoStr += QString(" *%1:[%2]*").arg(n).arg(subs[i]);
4611  else
4612  infoStr += QString(" %1:[%2]").arg(n).arg(subs[i]);
4613  n++;
4614  }
4615 
4617  for (int i = 0; i < subs.size(); i++)
4618  {
4619  if ((subtype & kDisplayRawTextSubtitle) && m_player->GetTrack(kTrackTypeRawText) == i)
4620  infoStr += QString(" *%1:[%2]*").arg(n).arg(subs[i]);
4621  else
4622  infoStr += QString(" %1:[%2]").arg(n).arg(subs[i]);
4623  n++;
4624  }
4625 
4626  m_playerContext.UnlockPlayingInfo(__FILE__, __LINE__);
4627 
4628  QString message = QString("NETWORK_CONTROL ANSWER %1").arg(infoStr);
4629  MythEvent me(message);
4630  gCoreContext->dispatch(me);
4631  }
4632  else if (tokens[2] == "VOLUME")
4633  {
4634  QString infoStr = QString("%1%").arg(m_audioState.m_volume);
4635  QString message = QString("NETWORK_CONTROL ANSWER %1").arg(infoStr);
4636  MythEvent me(message);
4637  gCoreContext->dispatch(me);
4638  }
4639  }
4640 }
4641 
4642 bool TV::StartPlayer(TVState desiredState)
4643 {
4644  LOG(VB_PLAYBACK, LOG_DEBUG, LOC + QString("(%1) -- begin").arg(StateToString(desiredState)));
4645 
4646  bool ok = CreatePlayer(desiredState);
4648 
4649  if (ok)
4650  {
4651  LOG(VB_GENERAL, LOG_INFO, LOC + QString("Created player."));
4652  SetSpeedChangeTimer(25ms, __LINE__);
4653  }
4654  else
4655  {
4656  LOG(VB_GENERAL, LOG_CRIT, LOC + QString("Failed to create player."));
4657  }
4658 
4659  LOG(VB_PLAYBACK, LOG_DEBUG, LOC + QString("(%1) -- end %2")
4660  .arg(StateToString(desiredState), (ok) ? "ok" : "error"));
4661 
4662  return ok;
4663 }
4664 
4666 {
4667  m_playerContext.LockDeletePlayer(__FILE__, __LINE__);
4668  if (!m_player)
4669  {
4670  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
4671  return;
4672  }
4673 
4674  float time = 0.0;
4675 
4677  m_player->IsPaused())
4678  {
4680  time = StopFFRew();
4681  else if (m_player->IsPaused())
4683 
4687  }
4688  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
4689 
4690  DoPlayerSeek(time);
4692 
4694 
4695  SetSpeedChangeTimer(0ms, __LINE__);
4697 }
4698 
4700 {
4701 
4703  return 0.0F;
4704 
4706  float time = 0.0F;
4707 
4708  m_playerContext.LockDeletePlayer(__FILE__, __LINE__);
4709  if (!m_player)
4710  {
4711  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
4712  return 0.0F;
4713  }
4714  if (m_player->IsPaused())
4715  {
4717  }
4718  else
4719  {
4721  time = StopFFRew();
4722  m_player->Pause();
4723  }
4724  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
4725  return time;
4726 }
4727 
4728 void TV::DoTogglePauseFinish(float Time, bool ShowOSD)
4729 {
4730  if (!m_playerContext.HasPlayer())
4731  return;
4732 
4734  return;
4735 
4736  if (ContextIsPaused(__FILE__, __LINE__))
4737  {
4740 
4741  DoPlayerSeek(Time);
4742  if (ShowOSD)
4743  UpdateOSDSeekMessage(tr("Paused"), kOSDTimeout_None);
4745  }
4746  else
4747  {
4748  DoPlayerSeek(Time);
4749  if (ShowOSD)
4752  }
4753 
4754  SetSpeedChangeTimer(0ms, __LINE__);
4755 }
4756 
4764 {
4765  bool paused = false;
4766  int dummy = 0;
4767  TV* tv = AcquireRelease(dummy, true);
4768  if (tv)
4769  {
4770  tv->GetPlayerReadLock();
4771  PlayerContext* context = tv->GetPlayerContext();
4772  if (!context->IsErrored())
4773  {
4774  context->LockDeletePlayer(__FILE__, __LINE__);
4775  if (context->m_player)
4776  paused = context->m_player->IsPaused();
4777  context->UnlockDeletePlayer(__FILE__, __LINE__);
4778  }
4779  tv->ReturnPlayerLock();
4780  AcquireRelease(dummy, false);
4781  }
4782  return paused;
4783 }
4784 
4785 void TV::DoTogglePause(bool ShowOSD)
4786 {
4787  bool ignore = false;
4788  bool paused = false;
4789  m_playerContext.LockDeletePlayer(__FILE__, __LINE__);
4790  if (m_player)
4791  {
4792  ignore = m_player->GetEditMode();
4793  paused = m_player->IsPaused();
4794  }
4795  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
4796 
4797  if (paused)
4799  else
4801 
4802  if (!ignore)
4804  // Emit Pause or Unpaused signal
4806 }
4807 
4808 bool TV::DoPlayerSeek(float Time)
4809 {
4811  return false;
4812 
4813  if (Time > -0.001F && Time < +0.001F)
4814  return false;
4815 
4816  LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("%1 seconds").arg(static_cast<double>(Time)));
4817 
4818  m_playerContext.LockDeletePlayer(__FILE__, __LINE__);
4819  if (!m_player)
4820  {
4821  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
4822  return false;
4823  }
4824 
4826  {
4827  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
4828  return false;
4829  }
4830 
4831  emit PauseAudioUntilReady();
4832 
4833  bool res = false;
4834 
4835  if (Time > 0.0F)
4836  res = m_player->FastForward(Time);
4837  else if (Time < 0.0F)
4838  res = m_player->Rewind(-Time);
4839  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
4840 
4841  return res;
4842 }
4843 
4844 bool TV::DoPlayerSeekToFrame(uint64_t FrameNum)
4845 {
4847  return false;
4848 
4849  LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("%1").arg(FrameNum));
4850 
4851  m_playerContext.LockDeletePlayer(__FILE__, __LINE__);
4852  if (!m_player)
4853  {
4854  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
4855  return false;
4856  }
4857 
4859  {
4860  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
4861  return false;
4862  }
4863 
4864  emit PauseAudioUntilReady();
4865 
4866  bool res = m_player->JumpToFrame(FrameNum);
4867 
4868  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
4869 
4870  return res;
4871 }
4872 
4873 bool TV::SeekHandleAction(const QStringList& Actions, const bool IsDVD)
4874 {
4875  const int kRewind = 4;
4876  const int kForward = 8;
4877  const int kSticky = 16;
4878  const int kSlippery = 32;
4879  const int kRelative = 64;
4880  const int kAbsolute = 128;
4881  const int kIgnoreCutlist = 256;
4882  const int kWhenceMask = 3;
4883  int flags = 0;
4884  if (IsActionable(ACTION_SEEKFFWD, Actions))
4885  flags = ARBSEEK_FORWARD | kForward | kSlippery | kRelative;
4886  else if (IsActionable("FFWDSTICKY", Actions))
4887  flags = ARBSEEK_END | kForward | kSticky | kAbsolute;
4888  else if (IsActionable(ACTION_RIGHT, Actions))
4889  flags = ARBSEEK_FORWARD | kForward | kSticky | kRelative;
4890  else if (IsActionable(ACTION_SEEKRWND, Actions))
4891  flags = ARBSEEK_REWIND | kRewind | kSlippery | kRelative;
4892  else if (IsActionable("RWNDSTICKY", Actions))
4893  flags = ARBSEEK_SET | kRewind | kSticky | kAbsolute;
4894  else if (IsActionable(ACTION_LEFT, Actions))
4895  flags = ARBSEEK_REWIND | kRewind | kSticky | kRelative;
4896  else
4897  return false;
4898 
4899  int direction = (flags & kRewind) ? -1 : 1;
4900  if (HasQueuedInput())
4901  {
4902  DoArbSeek(static_cast<ArbSeekWhence>(flags & kWhenceMask), (flags & kIgnoreCutlist) == 0);
4903  }
4904  else if (ContextIsPaused(__FILE__, __LINE__))
4905  {
4906  if (!IsDVD)
4907  {
4908  QString message = (flags & kRewind) ? tr("Rewind") :
4909  tr("Forward");
4910  if (flags & kAbsolute) // FFWDSTICKY/RWNDSTICKY
4911  {
4912  float time = direction;
4913  DoSeek(time, message, /*timeIsOffset*/true, /*honorCutlist*/(flags & kIgnoreCutlist) == 0);
4914  }
4915  else
4916  {
4917  m_playerContext.LockDeletePlayer(__FILE__, __LINE__);
4918  uint64_t frameAbs = m_player->GetFramesPlayed();
4919  uint64_t frameRel = m_player->TranslatePositionAbsToRel(frameAbs);
4920  uint64_t targetRel = frameRel + static_cast<uint64_t>(direction);
4921  if (frameRel == 0 && direction < 0)
4922  targetRel = 0;
4923  uint64_t maxAbs = m_player->GetCurrentFrameCount();
4924  uint64_t maxRel = m_player->TranslatePositionAbsToRel(maxAbs);
4925  if (targetRel > maxRel)
4926  targetRel = maxRel;
4927  uint64_t targetAbs = m_player->TranslatePositionRelToAbs(targetRel);
4928  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
4929  DoPlayerSeekToFrame(targetAbs);
4931  }
4932  }
4933  }
4934  else if (flags & kSticky)
4935  {
4936  ChangeFFRew(direction);
4937  }
4938  else if (flags & kRewind)
4939  {
4940  if (m_smartForward)
4941  m_doSmartForward = true;
4942  DoSeek(-m_playerContext.m_rewtime, tr("Skip Back"), /*timeIsOffset*/true, /*honorCutlist*/(flags & kIgnoreCutlist) == 0);
4943  }
4944  else
4945  {
4947  {
4948  DoSeek(m_playerContext.m_rewtime, tr("Skip Ahead"), /*timeIsOffset*/true, /*honorCutlist*/(flags & kIgnoreCutlist) == 0);
4949  }
4950  else
4951  {
4952  DoSeek(m_playerContext.m_fftime, tr("Skip Ahead"), /*timeIsOffset*/true, /*honorCutlist*/(flags & kIgnoreCutlist) == 0);
4953  }
4954  }
4955  return true;
4956 }
4957 
4958 void TV::DoSeek(float Time, const QString &Msg, bool TimeIsOffset, bool HonorCutlist)
4959 {
4960  if (!m_player)
4961  return;
4962 
4963  bool limitkeys = false;
4964 
4965  m_playerContext.LockDeletePlayer(__FILE__, __LINE__);
4966  if (m_player->GetLimitKeyRepeat())
4967  limitkeys = true;
4968 
4969  if (!limitkeys || (m_keyRepeatTimer.elapsed() > kKeyRepeatTimeout))
4970  {
4972  NormalSpeed();
4973  Time += StopFFRew();
4974  if (TimeIsOffset)
4975  {
4976  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
4977  DoPlayerSeek(Time);
4978  }
4979  else
4980  {
4981  auto time = millisecondsFromFloat(Time * 1000);
4982  uint64_t desiredFrameRel = m_player->TranslatePositionMsToFrame(time, HonorCutlist);
4983  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
4984  DoPlayerSeekToFrame(desiredFrameRel);
4985  }
4986  bool paused = m_player->IsPaused();
4988  }
4989  else
4990  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
4991 }
4992 
4993 void TV::DoSeekAbsolute(long long Seconds, bool HonorCutlist)
4994 {
4995  m_playerContext.LockDeletePlayer(__FILE__, __LINE__);
4996  if (!m_player)
4997  {
4998  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
5000  return;
5001  }
5002  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
5003  DoSeek(Seconds, tr("Jump To"), /*timeIsOffset*/false, HonorCutlist);
5005 }
5006 
5007 void TV::DoArbSeek(ArbSeekWhence Whence, bool HonorCutlist)
5008 {
5009  bool ok = false;
5010  int seek = GetQueuedInputAsInt(&ok);
5011  ClearInputQueues(true);
5012  if (!ok)
5013  return;
5014 
5015  int64_t time = (int(seek / 100) * 3600) + ((seek % 100) * 60);
5016 
5017  if (Whence == ARBSEEK_FORWARD)
5018  {
5019  DoSeek(time, tr("Jump Ahead"), /*timeIsOffset*/true, HonorCutlist);
5020  }
5021  else if (Whence == ARBSEEK_REWIND)
5022  {
5023  DoSeek(-time, tr("Jump Back"), /*timeIsOffset*/true, HonorCutlist);
5024  }
5025  else if (Whence == ARBSEEK_END)
5026  {
5027  m_playerContext.LockDeletePlayer(__FILE__, __LINE__);
5028  if (!m_player)
5029  {
5030  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
5031  return;
5032  }
5033  uint64_t total_frames = m_player->GetCurrentFrameCount();
5034  float dur = m_player->ComputeSecs(total_frames, HonorCutlist);
5035  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
5036  DoSeek(std::max(0.0F, dur - static_cast<float>(time)), tr("Jump To"), /*timeIsOffset*/false, HonorCutlist);
5037  }
5038  else
5039  {
5040  DoSeekAbsolute(time, HonorCutlist);
5041  }
5042 }
5043 
5045 {
5047  return;
5048 
5050 
5051  m_playerContext.LockDeletePlayer(__FILE__, __LINE__);
5052  if (m_player)
5054  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
5055 
5056  SetSpeedChangeTimer(0ms, __LINE__);
5057 }
5058 
5059 void TV::ChangeSpeed(int Direction)
5060 {
5061  int old_speed = m_playerContext.m_ffRewSpeed;
5062 
5063  if (ContextIsPaused(__FILE__, __LINE__))
5065 
5066  m_playerContext.m_ffRewSpeed += Direction;
5067 
5068  float time = StopFFRew();
5069  float speed {NAN};
5070 
5071  // Make sure these values for m_ffRewSpeed in TV::ChangeSpeed()
5072  // and PlayerContext::GetPlayMessage() stay in sync.
5073  if (m_playerContext.m_ffRewSpeed == 0)
5074  speed = m_playerContext.m_tsNormal;
5075  else if (m_playerContext.m_ffRewSpeed == -1)
5076  speed = 1.0F / 3;
5077  else if (m_playerContext.m_ffRewSpeed == -2)
5078  speed = 1.0F / 8;
5079  else if (m_playerContext.m_ffRewSpeed == -3)
5080  speed = 1.0F / 16;
5081  else if (m_playerContext.m_ffRewSpeed == -4)
5082  {
5083  DoTogglePause(true);
5084  return;
5085  }
5086  else
5087  {
5088  m_playerContext.m_ffRewSpeed = old_speed;
5089  return;
5090  }
5091 
5092  m_playerContext.LockDeletePlayer(__FILE__, __LINE__);
5093  if (m_player && !m_player->Play(speed, m_playerContext.m_ffRewSpeed == 0))
5094  {
5095  m_playerContext.m_ffRewSpeed = old_speed;
5096  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
5097  return;
5098  }
5099  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
5100  DoPlayerSeek(time);
5101  QString mesg = m_playerContext.GetPlayMessage();
5103 
5104  SetSpeedChangeTimer(0ms, __LINE__);
5105 }
5106 
5108 {
5109  float time = 0.0;
5110 
5112  return time;
5113 
5114  if (m_playerContext.m_ffRewState > 0)
5115  time = -m_ffRewSpeeds[static_cast<size_t>(m_playerContext.m_ffRewIndex)] * m_ffRewRepos;
5116  else
5117  time = m_ffRewSpeeds[static_cast<size_t>(m_playerContext.m_ffRewIndex)] * m_ffRewRepos;
5118 
5121 
5122  m_playerContext.LockDeletePlayer(__FILE__, __LINE__);
5123  if (m_player)
5125  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
5126 
5127  SetSpeedChangeTimer(0ms, __LINE__);
5128 
5129  return time;
5130 }
5131 
5132 void TV::ChangeFFRew(int Direction)
5133 {
5134  if (m_playerContext.m_ffRewState == Direction)
5135  {
5136  while (++m_playerContext.m_ffRewIndex < static_cast<int>(m_ffRewSpeeds.size()))
5137  if (m_ffRewSpeeds[static_cast<size_t>(m_playerContext.m_ffRewIndex)])
5138  break;
5139  if (m_playerContext.m_ffRewIndex >= static_cast<int>(m_ffRewSpeeds.size()))
5142  }
5143  else if (!m_ffRewReverse && m_playerContext.m_ffRewState == -Direction)
5144  {
5146  if (m_ffRewSpeeds[static_cast<size_t>(m_playerContext.m_ffRewIndex)])
5147  break;
5150  else
5151  {
5152  float time = StopFFRew();
5153  DoPlayerSeek(time);
5155  }
5156  }
5157  else
5158  {
5159  NormalSpeed();
5160  m_playerContext.m_ffRewState = Direction;
5162  }
5163 }
5164 
5165 void TV::SetFFRew(int Index)
5166 {
5168  return;
5169 
5170  auto index = static_cast<size_t>(Index);
5171  if (!m_ffRewSpeeds[index])
5172  return;
5173 
5174  auto ffrewindex = static_cast<size_t>(m_playerContext.m_ffRewIndex);
5175  int speed = 0;
5176  QString mesg;
5177  if (m_playerContext.m_ffRewState > 0)
5178  {
5179  speed = m_ffRewSpeeds[index];
5180  // Don't allow ffwd if seeking is needed but not available
5182  return;
5183 
5184  m_playerContext.m_ffRewIndex = Index;
5185  mesg = tr("Forward %1X").arg(m_ffRewSpeeds[ffrewindex]);
5186  m_playerContext.m_ffRewSpeed = speed;
5187  }
5188  else
5189  {
5190  // Don't rewind if we cannot seek
5192  return;
5193 
5194  m_playerContext.m_ffRewIndex = Index;
5195  mesg = tr("Rewind %1X").arg(m_ffRewSpeeds[ffrewindex]);
5196  speed = -m_ffRewSpeeds[ffrewindex];
5197  m_playerContext.m_ffRewSpeed = speed;
5198  }
5199 
5200  m_playerContext.LockDeletePlayer(__FILE__, __LINE__);
5201  if (m_player)
5202  m_player->Play(static_cast<float>(speed), (speed == 1) && (m_playerContext.m_ffRewState > 0));
5203  m_playerContext.UnlockDeletePlayer(__FILE__, __LINE__);
5204 
5206 
5207  SetSpeedChangeTimer(0ms, __LINE__);
5208 }
5209 
5210 void TV::DoQueueTranscode(const QString& Profile)
5211 {
5212  m_playerContext.LockPlayingInfo(__FILE__, __LINE__);
5213 
5215  {
5216  bool stop = false;
5217  if (m_queuedTranscode ||