MythTV  master
mythtv/programs/mythtranscode/main.cpp
Go to the documentation of this file.
1 // C++ headers
2 #include <cerrno>
3 #include <fcntl.h> // for open flags
4 #include <fstream>
5 #include <iostream>
6 using namespace std;
7 
8 // Qt headers
9 #include <QCoreApplication>
10 #include <QDir>
11 #include <utility>
12 
13 // MythTV headers
14 #include "mythmiscutil.h"
15 #include "exitcodes.h"
16 #include "programinfo.h"
17 #include "jobqueue.h"
18 #include "mythcontext.h"
19 #include "mythdb.h"
20 #include "mythversion.h"
21 #include "mythdate.h"
22 #include "transcode.h"
23 #include "mpeg2fix.h"
24 #include "remotefile.h"
25 #include "mythtranslation.h"
26 #include "loggingserver.h"
27 #include "mythlogging.h"
28 #include "commandlineparser.h"
29 #include "recordinginfo.h"
30 #include "signalhandling.h"
31 #include "HLS/httplivestream.h"
32 #include "cleanupguard.h"
33 
34 static void CompleteJob(int jobID, ProgramInfo *pginfo, bool useCutlist,
35  frm_dir_map_t *deleteMap, int &exitCode,
36  int resultCode, bool forceDelete);
37 
38 static int glbl_jobID = -1;
39 static QString recorderOptions = "";
40 
41 static void UpdatePositionMap(frm_pos_map_t &posMap, frm_pos_map_t &durMap, const QString& mapfile,
42  ProgramInfo *pginfo)
43 {
44  if (pginfo && mapfile.isEmpty())
45  {
48  pginfo->SavePositionMap(posMap, MARK_GOP_BYFRAME);
49  pginfo->SavePositionMap(durMap, MARK_DURATION_MS);
50  }
51  else if (!mapfile.isEmpty())
52  {
53  MarkTypes keyType = MARK_GOP_BYFRAME;
54  FILE *mapfh = fopen(mapfile.toLocal8Bit().constData(), "w");
55  if (!mapfh)
56  {
57  LOG(VB_GENERAL, LOG_ERR, QString("Could not open map file '%1'")
58  .arg(mapfile) + ENO);
59  return;
60  }
61  frm_pos_map_t::const_iterator it;
62  fprintf (mapfh, "Type: %d\n", keyType);
63  for (it = posMap.begin(); it != posMap.end(); ++it)
64  {
65  QString str = QString("%1 %2\n").arg(it.key()).arg(*it);
66  fprintf(mapfh, "%s", qPrintable(str));
67  }
68  fclose(mapfh);
69  }
70 }
71 
72 static int BuildKeyframeIndex(MPEG2fixup *m2f, QString &infile,
73  frm_pos_map_t &posMap, frm_pos_map_t &durMap, int jobID)
74 {
75  if (!m2f)
76  return 0;
77 
78  if (jobID < 0 || JobQueue::GetJobCmd(jobID) != JOB_STOP)
79  {
80  if (jobID >= 0)
82  QObject::tr("Generating Keyframe Index"));
83  int err = m2f->BuildKeyframeIndex(infile, posMap, durMap);
84  if (err)
85  return err;
86  if (jobID >= 0)
88  QObject::tr("Transcode Completed"));
89  }
90  return 0;
91 }
92 
93 static void UpdateJobQueue(float percent_done)
94 {
96  QString("%1% ").arg(percent_done, 0, 'f', 1) +
97  QObject::tr("Completed"));
98 }
99 
100 static int CheckJobQueue()
101 {
103  {
104  LOG(VB_GENERAL, LOG_NOTICE, "Transcoding stopped by JobQueue");
105  return 1;
106  }
107  return 0;
108 }
109 
110 static int QueueTranscodeJob(ProgramInfo *pginfo, const QString& profile,
111  const QString& hostname, bool usecutlist)
112 {
113  RecordingInfo recinfo(*pginfo);
114  if (!profile.isEmpty())
116 
118  pginfo->GetRecordingStartTime(),
119  hostname, "", "",
120  usecutlist ? JOB_USE_CUTLIST : 0))
121  {
122  LOG(VB_GENERAL, LOG_NOTICE,
123  QString("Queued transcode job for chanid %1 @ %2")
124  .arg(pginfo->GetChanID())
126  return GENERIC_EXIT_OK;
127  }
128 
129  LOG(VB_GENERAL, LOG_ERR, QString("Error queuing job for chanid %1 @ %2")
130  .arg(pginfo->GetChanID())
132  return GENERIC_EXIT_DB_ERROR;
133 }
134 
135 namespace
136 {
137  void cleanup()
138  {
139  delete gContext;
140  gContext = nullptr;
142  }
143 }
144 
145 int main(int argc, char *argv[])
146 {
147  uint chanid = 0;
148  QDateTime starttime;
149  QString infile;
150  QString outfile;
151  QString profilename = QString("autodetect");
152  QString fifodir = nullptr;
153  int jobID = -1;
154  int jobType = JOB_NONE;
155  int otype = REPLEX_MPEG2;
156  bool useCutlist = false;
157  bool keyframesonly = false;
158  bool build_index = false;
159  bool fifosync = false;
160  bool mpeg2 = false;
161  bool fifo_info = false;
162  bool cleanCut = false;
163  QMap<QString, QString> settingsOverride;
164  frm_dir_map_t deleteMap;
165  frm_pos_map_t posMap;
166  frm_pos_map_t durMap;
167  int AudioTrackNo = -1;
168 
169  bool found_starttime = false;
170  bool found_chanid = false;
171  bool found_infile = false;
172  int update_index = 1;
173  bool isVideo = false;
174  bool passthru = false;
175 
177  if (!cmdline.Parse(argc, argv))
178  {
179  cmdline.PrintHelp();
181  }
182 
183  if (cmdline.toBool("showhelp"))
184  {
185  cmdline.PrintHelp();
186  return GENERIC_EXIT_OK;
187  }
188 
189  if (cmdline.toBool("showversion"))
190  {
192  return GENERIC_EXIT_OK;
193  }
194 
195  QCoreApplication a(argc, argv);
196  QCoreApplication::setApplicationName(MYTH_APPNAME_MYTHTRANSCODE);
197 
198  if (cmdline.toBool("outputfile"))
199  {
200  outfile = cmdline.toString("outputfile");
201  update_index = 0;
202  }
203 
204  bool showprogress = cmdline.toBool("showprogress");
205 
206  QString mask("general");
207  bool quiet = (outfile == "-") || showprogress;
208  int retval = cmdline.ConfigureLogging(mask, quiet);
209  if (retval != GENERIC_EXIT_OK)
210  return retval;
211 
212  if (cmdline.toBool("starttime"))
213  {
214  starttime = cmdline.toDateTime("starttime");
215  found_starttime = true;
216  }
217  if (cmdline.toBool("chanid"))
218  {
219  chanid = cmdline.toUInt("chanid");
220  found_chanid = true;
221  }
222  if (cmdline.toBool("jobid"))
223  jobID = cmdline.toInt("jobid");
224  if (cmdline.toBool("inputfile"))
225  {
226  infile = cmdline.toString("inputfile");
227  found_infile = true;
228  }
229  if (cmdline.toBool("video"))
230  isVideo = true;
231  if (cmdline.toBool("profile"))
232  profilename = cmdline.toString("profile");
233 
234  if (cmdline.toBool("usecutlist"))
235  {
236  useCutlist = true;
237  if (!cmdline.toString("usecutlist").isEmpty())
238  {
239  if (!cmdline.toBool("inputfile") && !cmdline.toBool("hls"))
240  {
241  LOG(VB_GENERAL, LOG_CRIT, "External cutlists are only allowed "
242  "when using the --infile option.");
244  }
245 
246  uint64_t last = 0;
247  QStringList cutlist = cmdline.toStringList("usecutlist", " ");
248  foreach (auto & cut, cutlist)
249  {
250  QStringList startend =
251  cut.split("-", QString::SkipEmptyParts);
252  if (startend.size() == 2)
253  {
254  uint64_t start = startend.first().toULongLong();
255  uint64_t end = startend.last().toULongLong();
256 
257  if (cmdline.toBool("inversecut"))
258  {
259  LOG(VB_GENERAL, LOG_DEBUG,
260  QString("Cutting section %1-%2.")
261  .arg(last).arg(start));
262  deleteMap[start] = MARK_CUT_END;
263  deleteMap[end] = MARK_CUT_START;
264  last = end;
265  }
266  else
267  {
268  LOG(VB_GENERAL, LOG_DEBUG,
269  QString("Cutting section %1-%2.")
270  .arg(start).arg(end));
271  deleteMap[start] = MARK_CUT_START;
272  deleteMap[end] = MARK_CUT_END;
273  }
274  }
275  }
276 
277  if (cmdline.toBool("inversecut"))
278  {
279  if (deleteMap.contains(0) && (deleteMap[0] == MARK_CUT_END))
280  deleteMap.remove(0);
281  else
282  deleteMap[0] = MARK_CUT_START;
283  deleteMap[999999999] = MARK_CUT_END;
284  LOG(VB_GENERAL, LOG_DEBUG,
285  QString("Cutting section %1-999999999.")
286  .arg(last));
287  }
288 
289  // sanitize cutlist
290  if (deleteMap.count() >= 2)
291  {
292  frm_dir_map_t::iterator cur = deleteMap.begin();
293  frm_dir_map_t::iterator prev;
294  prev = cur++;
295  while (cur != deleteMap.end())
296  {
297  if (prev.value() == cur.value())
298  {
299  // two of the same type next to each other
300  QString err("Cut %1points found at %3 and %4, with no "
301  "%2 point in between.");
302  if (prev.value() == MARK_CUT_END)
303  err = err.arg("end").arg("start");
304  else
305  err = err.arg("start").arg("end");
306  LOG(VB_GENERAL, LOG_CRIT, "Invalid cutlist defined!");
307  LOG(VB_GENERAL, LOG_CRIT, err.arg(prev.key())
308  .arg(cur.key()));
310  }
311  if ( (prev.value() == MARK_CUT_START) &&
312  ((cur.key() - prev.key()) < 2) )
313  {
314  LOG(VB_GENERAL, LOG_WARNING, QString("Discarding "
315  "insufficiently long cut: %1-%2")
316  .arg(prev.key()).arg(cur.key()));
317  prev = deleteMap.erase(prev);
318  cur = deleteMap.erase(cur);
319 
320  if (cur == deleteMap.end())
321  continue;
322  }
323  prev = cur++;
324  }
325  }
326  }
327  else if (cmdline.toBool("inversecut"))
328  {
329  cerr << "Cutlist inversion requires an external cutlist be" << endl
330  << "provided using the --honorcutlist option." << endl;
332  }
333  }
334 
335  if (cmdline.toBool("cleancut"))
336  cleanCut = true;
337 
338  if (cmdline.toBool("allkeys"))
339  keyframesonly = true;
340  if (cmdline.toBool("reindex"))
341  build_index = true;
342  if (cmdline.toBool("fifodir"))
343  fifodir = cmdline.toString("fifodir");
344  if (cmdline.toBool("fifoinfo"))
345  fifo_info = true;
346  if (cmdline.toBool("fifosync"))
347  fifosync = true;
348  if (cmdline.toBool("recopt"))
349  recorderOptions = cmdline.toString("recopt");
350  if (cmdline.toBool("mpeg2"))
351  mpeg2 = true;
352  if (cmdline.toBool("ostream"))
353  {
354  if (cmdline.toString("ostream") == "dvd")
355  otype = REPLEX_DVD;
356  else if (cmdline.toString("ostream") == "ps")
357  otype = REPLEX_MPEG2;
358  else if (cmdline.toString("ostream") == "ts")
359  otype = REPLEX_TS_SD;
360  else
361  {
362  cerr << "Invalid 'ostream' type: "
363  << cmdline.toString("ostream").toLocal8Bit().constData()
364  << endl;
366  }
367  }
368  if (cmdline.toBool("audiotrack"))
369  AudioTrackNo = cmdline.toInt("audiotrack");
370  if (cmdline.toBool("passthru"))
371  passthru = true;
372  // Set if we want to delete the original file once conversion succeeded.
373  bool deleteOriginal = cmdline.toBool("delete");
374 
375  CleanupGuard callCleanup(cleanup);
376 
377 #ifndef _WIN32
378  QList<int> signallist;
379  signallist << SIGINT << SIGTERM << SIGSEGV << SIGABRT << SIGBUS << SIGFPE
380  << SIGILL;
381 #if ! CONFIG_DARWIN
382  signallist << SIGRTMIN;
383 #endif
384  SignalHandler::Init(signallist);
385  SignalHandler::SetHandler(SIGHUP, logSigHup);
386 #endif
387 
388  // Load the context
390  if (!gContext->Init(false))
391  {
392  LOG(VB_GENERAL, LOG_ERR, "Failed to init MythContext, exiting.");
394  }
395 
396  MythTranslation::load("mythfrontend");
397 
399 
400  if (jobID != -1)
401  {
402  if (JobQueue::GetJobInfoFromID(jobID, jobType, chanid, starttime))
403  {
404  found_starttime = true;
405  found_chanid = true;
406  }
407  else
408  {
409  cerr << "mythtranscode: ERROR: Unable to find DB info for "
410  << "JobQueue ID# " << jobID << endl;
412  }
413  }
414 
415  if (((!found_infile && !(found_chanid && found_starttime)) ||
416  (found_infile && (found_chanid || found_starttime))) &&
417  (!cmdline.toBool("hls")))
418  {
419  cerr << "Must specify -i OR -c AND -s options!" << endl;
421  }
422  if (isVideo && !found_infile && !cmdline.toBool("hls"))
423  {
424  cerr << "Must specify --infile to use --video" << endl;
426  }
427  if (jobID >= 0 && (found_infile || build_index))
428  {
429  cerr << "Can't specify -j with --buildindex, --video or --infile"
430  << endl;
432  }
433  if ((jobID >= 0) && build_index)
434  {
435  cerr << "Can't specify both -j and --buildindex" << endl;
437  }
438  if (keyframesonly && !fifodir.isEmpty())
439  {
440  cerr << "Cannot specify both --fifodir and --allkeys" << endl;
442  }
443  if (fifosync && fifodir.isEmpty())
444  {
445  cerr << "Must specify --fifodir to use --fifosync" << endl;
447  }
448  if (fifo_info && !fifodir.isEmpty())
449  {
450  cerr << "Cannot specify both --fifodir and --fifoinfo" << endl;
452  }
453  if (cleanCut && fifodir.isEmpty() && !fifo_info)
454  {
455  cerr << "Clean cutting works only in fifodir mode" << endl;
457  }
458  if (cleanCut && !useCutlist)
459  {
460  cerr << "--cleancut is pointless without --honorcutlist" << endl;
462  }
463 
464  if (fifo_info)
465  {
466  // Setup a dummy fifodir path, so that the "fifodir" code path
467  // is taken. The path wont actually be used.
468  fifodir = "DummyFifoPath";
469  }
470 
472  {
473  LOG(VB_GENERAL, LOG_ERR, "couldn't open db");
474  return GENERIC_EXIT_DB_ERROR;
475  }
476 
477  ProgramInfo *pginfo = nullptr;
478  if (cmdline.toBool("hls"))
479  {
480  if (cmdline.toBool("hlsstreamid"))
481  {
482  HTTPLiveStream hls(cmdline.toInt("hlsstreamid"));
483  pginfo = new ProgramInfo(hls.GetSourceFile());
484  }
485  if (pginfo == nullptr)
486  pginfo = new ProgramInfo();
487  }
488  else if (isVideo)
489  {
490  // We want the absolute file path for the filemarkup table
491  QFileInfo inf(infile);
492  infile = inf.absoluteFilePath();
493  pginfo = new ProgramInfo(infile);
494  }
495  else if (!found_infile)
496  {
497  pginfo = new ProgramInfo(chanid, starttime);
498 
499  if (!pginfo->GetChanID())
500  {
501  LOG(VB_GENERAL, LOG_ERR,
502  QString("Couldn't find recording for chanid %1 @ %2")
503  .arg(chanid).arg(starttime.toString(Qt::ISODate)));
504  delete pginfo;
506  }
507 
508  infile = pginfo->GetPlaybackURL(false, true);
509  }
510  else
511  {
512  pginfo = new ProgramInfo(infile);
513  if (!pginfo->GetChanID())
514  {
515  LOG(VB_GENERAL, LOG_ERR,
516  QString("Couldn't find a recording for filename '%1'")
517  .arg(infile));
518  delete pginfo;
520  }
521  }
522 
523  if (!pginfo)
524  {
525  LOG(VB_GENERAL, LOG_ERR, "No program info found!");
527  }
528 
529  if (cmdline.toBool("queue"))
530  {
531  QString hostname = cmdline.toString("queue");
532  return QueueTranscodeJob(pginfo, profilename, hostname, useCutlist);
533  }
534 
535  if (infile.startsWith("myth://") && (outfile.isEmpty() || outfile != "-") &&
536  fifodir.isEmpty() && !cmdline.toBool("hls") && !cmdline.toBool("avf"))
537  {
538  LOG(VB_GENERAL, LOG_ERR,
539  QString("Attempted to transcode %1. Mythtranscode is currently "
540  "unable to transcode remote files.") .arg(infile));
541  delete pginfo;
543  }
544 
545  if (outfile.isEmpty() && !build_index && fifodir.isEmpty())
546  outfile = infile + ".tmp";
547 
548  if (jobID >= 0)
549  JobQueue::ChangeJobStatus(jobID, JOB_RUNNING);
550 
551  auto *transcode = new Transcode(pginfo);
552 
553  if (!build_index)
554  {
555  if (cmdline.toBool("hlsstreamid"))
556  {
557  LOG(VB_GENERAL, LOG_NOTICE,
558  QString("Transcoding HTTP Live Stream ID %1")
559  .arg(cmdline.toInt("hlsstreamid")));
560  }
561  else if (fifodir.isEmpty())
562  {
563  LOG(VB_GENERAL, LOG_NOTICE, QString("Transcoding from %1 to %2")
564  .arg(infile).arg(outfile));
565  }
566  else
567  {
568  LOG(VB_GENERAL, LOG_NOTICE, QString("Transcoding from %1 to FIFO")
569  .arg(infile));
570  }
571  }
572 
573  if (cmdline.toBool("avf"))
574  {
575  transcode->SetAVFMode();
576 
577  if (cmdline.toBool("container"))
578  transcode->SetCMDContainer(cmdline.toString("container"));
579  if (cmdline.toBool("acodec"))
580  transcode->SetCMDAudioCodec(cmdline.toString("acodec"));
581  if (cmdline.toBool("vcodec"))
582  transcode->SetCMDVideoCodec(cmdline.toString("vcodec"));
583  }
584  else if (cmdline.toBool("hls"))
585  {
586  transcode->SetHLSMode();
587 
588  if (cmdline.toBool("hlsstreamid"))
589  transcode->SetHLSStreamID(cmdline.toInt("hlsstreamid"));
590  if (cmdline.toBool("maxsegments"))
591  transcode->SetHLSMaxSegments(cmdline.toInt("maxsegments"));
592  if (cmdline.toBool("noaudioonly"))
593  transcode->DisableAudioOnlyHLS();
594  }
595 
596  if (cmdline.toBool("avf") || cmdline.toBool("hls"))
597  {
598  if (cmdline.toBool("width"))
599  transcode->SetCMDWidth(cmdline.toInt("width"));
600  if (cmdline.toBool("height"))
601  transcode->SetCMDHeight(cmdline.toInt("height"));
602  if (cmdline.toBool("bitrate"))
603  transcode->SetCMDBitrate(cmdline.toInt("bitrate") * 1000);
604  if (cmdline.toBool("audiobitrate"))
605  transcode->SetCMDAudioBitrate(cmdline.toInt("audiobitrate") * 1000);
606  }
607 
608  if (showprogress)
609  transcode->ShowProgress(true);
610  if (!recorderOptions.isEmpty())
611  transcode->SetRecorderOptions(recorderOptions);
612  int result = 0;
613  if ((!mpeg2 && !build_index) || cmdline.toBool("hls"))
614  {
615  result = transcode->TranscodeFile(infile, outfile,
616  profilename, useCutlist,
617  (fifosync || keyframesonly), jobID,
618  fifodir, fifo_info, cleanCut, deleteMap,
619  AudioTrackNo, passthru);
620 
621  if ((result == REENCODE_OK) && (jobID >= 0))
622  {
623  JobQueue::ChangeJobArgs(jobID, "RENAME_TO_NUV");
624  RecordingInfo recInfo(pginfo->GetRecordingID());
625  RecordingFile *recFile = recInfo.GetRecordingFile();
626  recFile->m_containerFormat = formatNUV;
627  recFile->Save();
628  }
629  }
630 
631  if (fifo_info)
632  {
633  delete transcode;
634  return GENERIC_EXIT_OK;
635  }
636 
637  int exitcode = GENERIC_EXIT_OK;
638  if ((result == REENCODE_MPEG2TRANS) || mpeg2 || build_index)
639  {
640  void (*update_func)(float) = nullptr;
641  int (*check_func)() = nullptr;
642  if (useCutlist)
643  {
644  LOG(VB_GENERAL, LOG_INFO, "Honoring the cutlist while transcoding");
645  if (deleteMap.isEmpty())
646  pginfo->QueryCutList(deleteMap);
647  }
648  if (jobID >= 0)
649  {
650  glbl_jobID = jobID;
651  update_func = &UpdateJobQueue;
652  check_func = &CheckJobQueue;
653  }
654 
655  auto *m2f = new MPEG2fixup(infile, outfile,
656  &deleteMap, nullptr, false, false, 20,
657  showprogress, otype, update_func,
658  check_func);
659 
660  if (cmdline.toBool("allaudio"))
661  {
662  m2f->SetAllAudio(true);
663  }
664 
665  if (build_index)
666  {
667  int err = BuildKeyframeIndex(m2f, infile, posMap, durMap, jobID);
668  if (err)
669  {
670  delete m2f;
671  m2f = nullptr;
672  return err;
673  }
674  if (update_index)
675  UpdatePositionMap(posMap, durMap, nullptr, pginfo);
676  else
677  UpdatePositionMap(posMap, durMap, outfile + QString(".map"), pginfo);
678  }
679  else
680  {
681  result = m2f->Start();
682  if (result == REENCODE_OK)
683  {
684  result = BuildKeyframeIndex(m2f, outfile, posMap, durMap, jobID);
685  if (result == REENCODE_OK)
686  {
687  if (update_index)
688  UpdatePositionMap(posMap, durMap, nullptr, pginfo);
689  else
690  UpdatePositionMap(posMap, durMap, outfile + QString(".map"),
691  pginfo);
692  }
693  RecordingInfo recInfo(*pginfo);
694  RecordingFile *recFile = recInfo.GetRecordingFile();
695  if (otype == REPLEX_DVD || otype == REPLEX_MPEG2 ||
696  otype == REPLEX_HDTV)
697  {
699  JobQueue::ChangeJobArgs(jobID, "RENAME_TO_MPG");
700  }
701  else
702  {
704  }
705  recFile->Save();
706  }
707  }
708  delete m2f;
709  m2f = nullptr;
710  }
711 
712  if (result == REENCODE_OK)
713  {
714  if (jobID >= 0)
715  JobQueue::ChangeJobStatus(jobID, JOB_STOPPING);
716  LOG(VB_GENERAL, LOG_NOTICE, QString("%1 %2 done")
717  .arg(build_index ? "Building Index for" : "Transcoding")
718  .arg(infile));
719  }
720  else if (result == REENCODE_CUTLIST_CHANGE)
721  {
722  if (jobID >= 0)
723  JobQueue::ChangeJobStatus(jobID, JOB_RETRY);
724  LOG(VB_GENERAL, LOG_NOTICE,
725  QString("Transcoding %1 aborted because of cutlist update")
726  .arg(infile));
727  exitcode = GENERIC_EXIT_RESTART;
728  }
729  else if (result == REENCODE_STOPPED)
730  {
731  if (jobID >= 0)
732  JobQueue::ChangeJobStatus(jobID, JOB_ABORTING);
733  LOG(VB_GENERAL, LOG_NOTICE,
734  QString("Transcoding %1 stopped because of stop command")
735  .arg(infile));
736  exitcode = GENERIC_EXIT_KILLED;
737  }
738  else
739  {
740  if (jobID >= 0)
741  JobQueue::ChangeJobStatus(jobID, JOB_ERRORING);
742  LOG(VB_GENERAL, LOG_ERR, QString("Transcoding %1 failed").arg(infile));
743  exitcode = result;
744  }
745 
746  if (deleteOriginal || jobID >= 0)
747  CompleteJob(jobID, pginfo, useCutlist, &deleteMap, exitcode, result, deleteOriginal);
748 
749  transcode->deleteLater();
750 
751  return exitcode;
752 }
753 
754 static int transUnlink(const QString& filename, ProgramInfo *pginfo)
755 {
756  QString hostname = pginfo->GetHostname();
757 
758  if (!pginfo->GetStorageGroup().isEmpty() &&
759  !hostname.isEmpty())
760  {
762  QString basename = filename.section('/', -1);
763  QString uri = MythCoreContext::GenMythURL(hostname, port, basename,
764  pginfo->GetStorageGroup());
765 
766  LOG(VB_GENERAL, LOG_NOTICE, QString("Requesting delete for file '%1'.")
767  .arg(uri));
768  bool ok = RemoteFile::DeleteFile(uri);
769  if (ok)
770  return 0;
771  }
772 
773  LOG(VB_GENERAL, LOG_NOTICE, QString("Deleting file '%1'.").arg(filename));
774  return unlink(filename.toLocal8Bit().constData());
775 }
776 
777 static uint64_t ComputeNewBookmark(uint64_t oldBookmark,
778  frm_dir_map_t *deleteMap)
779 {
780  if (deleteMap == nullptr)
781  return oldBookmark;
782 
783  uint64_t subtraction = 0;
784  uint64_t startOfCutRegion = 0;
785  frm_dir_map_t delMap = *deleteMap;
786  bool withinCut = false;
787  bool firstMark = true;
788  while (!delMap.empty() && delMap.begin().key() <= oldBookmark)
789  {
790  uint64_t key = delMap.begin().key();
791  MarkTypes mark = delMap.begin().value();
792 
793  if (mark == MARK_CUT_START && !withinCut)
794  {
795  withinCut = true;
796  startOfCutRegion = key;
797  }
798  else if (mark == MARK_CUT_END && firstMark)
799  {
800  subtraction += key;
801  }
802  else if (mark == MARK_CUT_END && withinCut)
803  {
804  withinCut = false;
805  subtraction += (key - startOfCutRegion);
806  }
807  delMap.remove(key);
808  firstMark = false;
809  }
810  if (withinCut)
811  subtraction += (oldBookmark - startOfCutRegion);
812  return oldBookmark - subtraction;
813 }
814 
815 static uint64_t ReloadBookmark(ProgramInfo *pginfo)
816 {
817  MSqlQuery query(MSqlQuery::InitCon());
818  uint64_t currentBookmark = 0;
819  query.prepare("SELECT DISTINCT mark FROM recordedmarkup "
820  "WHERE chanid = :CHANID "
821  "AND starttime = :STARTIME "
822  "AND type = :MARKTYPE ;");
823  query.bindValue(":CHANID", pginfo->GetChanID());
824  query.bindValue(":STARTTIME", pginfo->GetRecordingStartTime());
825  query.bindValue(":MARKTYPE", MARK_BOOKMARK);
826  if (query.exec() && query.next())
827  {
828  currentBookmark = query.value(0).toLongLong();
829  }
830  return currentBookmark;
831 }
832 
833 static void WaitToDelete(ProgramInfo *pginfo)
834 {
835  LOG(VB_GENERAL, LOG_NOTICE,
836  "Transcode: delete old file: waiting while program is in use.");
837 
838  bool inUse = true;
839  MSqlQuery query(MSqlQuery::InitCon());
840  while (inUse)
841  {
842  query.prepare("SELECT count(*) FROM inuseprograms "
843  "WHERE chanid = :CHANID "
844  "AND starttime = :STARTTIME "
845  "AND recusage = 'player' ;");
846  query.bindValue(":CHANID", pginfo->GetChanID());
847  query.bindValue(":STARTTIME", pginfo->GetRecordingStartTime());
848  if (!query.exec() || !query.next())
849  {
850  LOG(VB_GENERAL, LOG_ERR,
851  "Transcode: delete old file: in-use query failed;");
852  inUse = false;
853  }
854  else
855  {
856  inUse = (query.value(0).toUInt() != 0);
857  }
858 
859  if (inUse)
860  {
861  const unsigned kSecondsToWait = 10;
862  LOG(VB_GENERAL, LOG_NOTICE,
863  QString("Transcode: program in use, rechecking in %1 seconds.")
864  .arg(kSecondsToWait));
865  sleep(kSecondsToWait);
866  }
867  }
868  LOG(VB_GENERAL, LOG_NOTICE, "Transcode: program is no longer in use.");
869 }
870 
871 static void CompleteJob(int jobID, ProgramInfo *pginfo, bool useCutlist,
872  frm_dir_map_t *deleteMap, int &exitCode, int resultCode, bool forceDelete)
873 {
874  int status = JOB_UNKNOWN;
875  if (jobID >= 0)
876  status = JobQueue::GetJobStatus(jobID);
877 
878  if (!pginfo)
879  {
880  if (jobID >= 0)
881  JobQueue::ChangeJobStatus(jobID, JOB_ERRORED,
882  QObject::tr("Job errored, unable to find Program Info for job"));
883  LOG(VB_GENERAL, LOG_CRIT, "MythTranscode: Cleanup errored, unable to find Program Info");
884  return;
885  }
886 
887  const QString filename = pginfo->GetPlaybackURL(false, true);
888  const QByteArray fname = filename.toLocal8Bit();
889 
890  if (resultCode == REENCODE_OK)
891  {
892  WaitToDelete(pginfo);
893 
894  // Transcoding may take several minutes. Reload the bookmark
895  // in case it changed, then save its translated value back.
896  uint64_t previousBookmark =
897  ComputeNewBookmark(ReloadBookmark(pginfo), deleteMap);
898  pginfo->SaveBookmark(previousBookmark);
899 
900  QString jobArgs;
901  if (jobID >= 0)
902  jobArgs = JobQueue::GetJobArgs(jobID);
903 
904  const QString tmpfile = filename + ".tmp";
905  const QByteArray atmpfile = tmpfile.toLocal8Bit();
906 
907  // To save the original file...
908  const QString oldfile = filename + ".old";
909  const QByteArray aoldfile = oldfile.toLocal8Bit();
910 
911  QFileInfo st(tmpfile);
912  qint64 newSize = 0;
913  if (st.exists())
914  newSize = st.size();
915 
916  QString cnf = filename;
917  if (jobID >= 0)
918  {
919  if (filename.endsWith(".mpg") && jobArgs == "RENAME_TO_NUV")
920  {
921  QString newbase = pginfo->QueryBasename();
922  cnf.replace(".mpg", ".nuv");
923  newbase.replace(".mpg", ".nuv");
924  pginfo->SaveBasename(newbase);
925  }
926  else if (filename.endsWith(".ts") &&
927  (jobArgs == "RENAME_TO_MPG"))
928  {
929  QString newbase = pginfo->QueryBasename();
930  // MPEG-TS to MPEG-PS
931  cnf.replace(".ts", ".mpg");
932  newbase.replace(".ts", ".mpg");
933  pginfo->SaveBasename(newbase);
934  }
935  }
936 
937  const QString newfile = cnf;
938  const QByteArray anewfile = newfile.toLocal8Bit();
939 
940  if (rename(fname.constData(), aoldfile.constData()) == -1)
941  {
942  LOG(VB_GENERAL, LOG_ERR,
943  QString("mythtranscode: Error Renaming '%1' to '%2'")
944  .arg(filename).arg(oldfile) + ENO);
945  }
946 
947  if (rename(atmpfile.constData(), anewfile.constData()) == -1)
948  {
949  LOG(VB_GENERAL, LOG_ERR,
950  QString("mythtranscode: Error Renaming '%1' to '%2'")
951  .arg(tmpfile).arg(newfile) + ENO);
952  }
953 
954  if (!gCoreContext->GetBoolSetting("SaveTranscoding", false) || forceDelete)
955  {
956  bool followLinks =
957  gCoreContext->GetBoolSetting("DeletesFollowLinks", false);
958 
959  LOG(VB_FILE, LOG_INFO,
960  QString("mythtranscode: About to unlink/delete file: %1")
961  .arg(oldfile));
962 
963  QFileInfo finfo(oldfile);
964  if (followLinks && finfo.isSymLink())
965  {
966  QString link = getSymlinkTarget(oldfile);
967  QByteArray alink = link.toLocal8Bit();
968  int err = transUnlink(alink.constData(), pginfo);
969  if (err)
970  {
971  LOG(VB_GENERAL, LOG_ERR,
972  QString("mythtranscode: Error deleting '%1' "
973  "pointed to by '%2'")
974  .arg(alink.constData())
975  .arg(aoldfile.constData()) + ENO);
976  }
977 
978  err = unlink(aoldfile.constData());
979  if (err)
980  {
981  LOG(VB_GENERAL, LOG_ERR,
982  QString("mythtranscode: Error deleting '%1', "
983  "a link pointing to '%2'")
984  .arg(aoldfile.constData())
985  .arg(alink.constData()) + ENO);
986  }
987  }
988  else
989  {
990  int err = transUnlink(aoldfile.constData(), pginfo);
991  if (err)
992  {
993  LOG(VB_GENERAL, LOG_ERR,
994  QString("mythtranscode: Error deleting '%1': ")
995  .arg(oldfile) + ENO);
996  }
997  }
998  }
999 
1000  // Rename or delete all preview thumbnails.
1001  //
1002  // TODO: This cleanup should be moved to RecordingInfo, and triggered
1003  // when SaveBasename() is called
1004  QFileInfo fInfo(filename);
1005  QStringList nameFilters;
1006  nameFilters.push_back(fInfo.fileName() + "*.png");
1007  nameFilters.push_back(fInfo.fileName() + "*.jpg");
1008 
1009  QDir dir (fInfo.path());
1010  QFileInfoList previewFiles = dir.entryInfoList(nameFilters);
1011 
1012  foreach (const auto & previewFile, previewFiles)
1013  {
1014  QString oldFileName = previewFile.absoluteFilePath();
1015 
1016  // Delete previews if cutlist was applied. They will be re-created as
1017  // required. This prevents the user from being stuck with a preview
1018  // from a cut area and ensures that the "dimensioned" previews
1019  // correspond to the new timeline
1020  if (useCutlist)
1021  {
1022  // If unlink fails, keeping the old preview is not a problem.
1023  // The RENAME_TO_NUV check below will attempt to rename the
1024  // file, if required.
1025  if (transUnlink(oldFileName.toLocal8Bit().constData(), pginfo) != -1)
1026  continue;
1027  }
1028 
1029  if (jobArgs == "RENAME_TO_NUV" || jobArgs == "RENAME_TO_MPG")
1030  {
1031  QString newExtension = "mpg";
1032  if (jobArgs == "RENAME_TO_NUV")
1033  newExtension = "nuv";
1034 
1035  QString oldSuffix = previewFile.completeSuffix();
1036 
1037  if (!oldSuffix.startsWith(newExtension))
1038  {
1039  QString newSuffix = oldSuffix;
1040  QString oldExtension = oldSuffix.section(".", 0, 0);
1041  newSuffix.replace(oldExtension, newExtension);
1042 
1043  QString newFileName = oldFileName;
1044  newFileName.replace(oldSuffix, newSuffix);
1045 
1046  if (!QFile::rename(oldFileName, newFileName))
1047  {
1048  LOG(VB_GENERAL, LOG_ERR,
1049  QString("mythtranscode: Error renaming %1 to %2")
1050  .arg(oldFileName).arg(newFileName));
1051  }
1052  }
1053  }
1054  }
1055 
1056  MSqlQuery query(MSqlQuery::InitCon());
1057 
1058  if (useCutlist)
1059  {
1060  query.prepare("DELETE FROM recordedmarkup "
1061  "WHERE chanid = :CHANID "
1062  "AND starttime = :STARTTIME "
1063  "AND type != :BOOKMARK ");
1064  query.bindValue(":CHANID", pginfo->GetChanID());
1065  query.bindValue(":STARTTIME", pginfo->GetRecordingStartTime());
1066  query.bindValue(":BOOKMARK", MARK_BOOKMARK);
1067 
1068  if (!query.exec())
1069  MythDB::DBError("Error in mythtranscode", query);
1070 
1071  query.prepare("UPDATE recorded "
1072  "SET cutlist = :CUTLIST "
1073  "WHERE chanid = :CHANID "
1074  "AND starttime = :STARTTIME ;");
1075  query.bindValue(":CUTLIST", "0");
1076  query.bindValue(":CHANID", pginfo->GetChanID());
1077  query.bindValue(":STARTTIME", pginfo->GetRecordingStartTime());
1078 
1079  if (!query.exec())
1080  MythDB::DBError("Error in mythtranscode", query);
1081 
1083  }
1084  else
1085  {
1086  query.prepare("DELETE FROM recordedmarkup "
1087  "WHERE chanid = :CHANID "
1088  "AND starttime = :STARTTIME "
1089  "AND type not in ( :COMM_START, "
1090  " :COMM_END, :BOOKMARK, "
1091  " :CUTLIST_START, :CUTLIST_END) ;");
1092  query.bindValue(":CHANID", pginfo->GetChanID());
1093  query.bindValue(":STARTTIME", pginfo->GetRecordingStartTime());
1094  query.bindValue(":COMM_START", MARK_COMM_START);
1095  query.bindValue(":COMM_END", MARK_COMM_END);
1096  query.bindValue(":BOOKMARK", MARK_BOOKMARK);
1097  query.bindValue(":CUTLIST_START", MARK_CUT_START);
1098  query.bindValue(":CUTLIST_END", MARK_CUT_END);
1099 
1100  if (!query.exec())
1101  MythDB::DBError("Error in mythtranscode", query);
1102  }
1103 
1104  if (newSize)
1105  pginfo->SaveFilesize(newSize);
1106 
1107  if (jobID >= 0)
1108  JobQueue::ChangeJobStatus(jobID, JOB_FINISHED);
1109  }
1110  else
1111  {
1112  // Not a successful run, so remove the files we created
1113  QString filename_tmp = filename + ".tmp";
1114  QByteArray fname_tmp = filename_tmp.toLocal8Bit();
1115  LOG(VB_GENERAL, LOG_NOTICE, QString("Deleting %1").arg(filename_tmp));
1116  transUnlink(fname_tmp.constData(), pginfo);
1117 
1118  QString filename_map = filename + ".tmp.map";
1119  QByteArray fname_map = filename_map.toLocal8Bit();
1120  unlink(fname_map.constData());
1121 
1122  if (jobID >= 0)
1123  {
1124  if (status == JOB_ABORTING) // Stop command was sent
1125  {
1126  JobQueue::ChangeJobStatus(jobID, JOB_ABORTED,
1127  QObject::tr("Job Aborted"));
1128  }
1129  else if (status != JOB_ERRORING) // Recoverable error
1130  {
1131  exitCode = GENERIC_EXIT_RESTART;
1132  }
1133  else // Unrecoverable error
1134  {
1135  JobQueue::ChangeJobStatus(jobID, JOB_ERRORED,
1136  QObject::tr("Unrecoverable error"));
1137  }
1138  }
1139  }
1140 }
1141 /* vim: set expandtab tabstop=4 shiftwidth=4: */
QDateTime toDateTime(const QString &key) const
Returns stored QVariant as a QDateTime, falling to default if not provided.
#define GENERIC_EXIT_DB_ERROR
Database error.
Definition: exitcodes.h:17
bool next(void)
Wrap QSqlQuery::next() so we can display the query results.
Definition: mythdbcon.cpp:783
Startup context for MythTV.
Definition: mythcontext.h:42
#define REENCODE_MPEG2TRANS
Definition: transcodedefs.h:4
static bool DeleteFile(const QString &url)
Definition: remotefile.cpp:417
void bindValue(const QString &placeholder, const QVariant &val)
Add a single binding.
Definition: mythdbcon.cpp:864
#define GENERIC_EXIT_OK
Exited with no error.
Definition: exitcodes.h:10
#define GENERIC_EXIT_REMOTE_FILE
Can't transcode a remote file.
Definition: exitcodes.h:30
static uint64_t ComputeNewBookmark(uint64_t oldBookmark, frm_dir_map_t *deleteMap)
bool toBool(const QString &key) const
Returns stored QVariant as a boolean.
static void PrintVersion(void)
Print application version information.
void SaveCommFlagged(CommFlagStatus flag)
Set "commflagged" field in "recorded" table to "flag".
int BuildKeyframeIndex(QString &file, frm_pos_map_t &posMap, frm_pos_map_t &durMap)
Definition: mpeg2fix.cpp:2744
QSqlQuery wrapper that fetches a DB connection from the connection pool.
Definition: mythdbcon.h:125
void PrintHelp(void) const
Print command line option help.
#define GENERIC_EXIT_RESTART
Need to restart transcoding.
Definition: exitcodes.h:31
#define GENERIC_EXIT_KILLED
Process killed or stopped.
Definition: exitcodes.h:23
static void Init(QList< int > &signallist, QObject *parent=nullptr)
static void WaitToDelete(ProgramInfo *pginfo)
static enum JobCmds GetJobCmd(int jobID)
Definition: jobqueue.cpp:1459
Holds information on a TV Program one might wish to record.
Definition: recordinginfo.h:34
QString getSymlinkTarget(const QString &start_file, QStringList *intermediaries, unsigned maxLinks)
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
Definition: lang.c:20
void SavePositionMap(frm_pos_map_t &posMap, MarkTypes type, int64_t min_frame=-1, int64_t max_frame=-1) const
#define REPLEX_HDTV
Definition: multiplex.h:40
RecordingFile * GetRecordingFile() const
MythContext * gContext
This global variable contains the MythContext instance for the application.
Definition: mythcontext.cpp:62
static bool isVideo(const QString &mimeType)
Definition: newssite.cpp:293
static QString recorderOptions
unsigned sleep(unsigned int x)
Definition: compat.h:159
QString GetStorageGroup(void) const
Definition: programinfo.h:416
int GetBackendServerPort(void)
Returns the locally defined backend control port.
QMap< long long, long long > frm_pos_map_t
Frame # -> File offset map.
Definition: programtypes.h:46
static bool ChangeJobComment(int jobID, const QString &comment="")
Definition: jobqueue.cpp:1013
QVariant value(int i) const
Definition: mythdbcon.h:198
MarkTypes
Definition: programtypes.h:48
void ApplySettingsOverride(void)
Apply all overrides to the global context.
Holds information on recordings and videos.
Definition: programinfo.h:67
static int transUnlink(const QString &filename, ProgramInfo *pginfo)
virtual void SaveFilesize(uint64_t fsize)
Sets recording file size in database, and sets "filesize" field.
static bool GetJobInfoFromID(int jobID, int &jobType, uint &chanid, QDateTime &recstartts)
Definition: jobqueue.cpp:668
static int QueueTranscodeJob(ProgramInfo *pginfo, const QString &profile, const QString &hostname, bool usecutlist)
#define GENERIC_EXIT_NO_RECORDING_DATA
No program/recording data.
Definition: exitcodes.h:29
static void CompleteJob(int jobID, ProgramInfo *pginfo, bool useCutlist, frm_dir_map_t *deleteMap, int &exitCode, int resultCode, bool forceDelete)
static QString GenMythURL(const QString &host=QString(), int port=0, QString path=QString(), const QString &storageGroup=QString())
QString toString(const QString &key) const
Returns stored QVariant as a QString, falling to default if not provided.
int main(int argc, char *argv[])
bool SaveBasename(const QString &basename)
Sets a recording's basename in the database.
#define REENCODE_OK
Definition: transcodedefs.h:6
static void load(const QString &module_name)
Load a QTranslator for the user's preferred language.
static bool testDBConnection()
Checks DB connection + login (login info via Mythcontext)
Definition: mythdbcon.cpp:852
uint GetRecordingID(void) const
Definition: programinfo.h:440
void SaveBookmark(uint64_t frame)
TODO Move to RecordingInfo.
string hostname
Definition: caa.py:17
static void Done(void)
unsigned int uint
Definition: compat.h:140
static void SetHandler(int signum, SigHandlerFunc handler)
static MSqlQueryInfo InitCon(ConnectionReuse _reuse=kNormalConnection)
Only use this in combination with MSqlQuery constructor.
Definition: mythdbcon.cpp:535
MythCommFlagCommandLineParser cmdline
static int BuildKeyframeIndex(MPEG2fixup *m2f, QString &infile, frm_pos_map_t &posMap, frm_pos_map_t &durMap, int jobID)
Holds information on a recording file and it's video and audio streams.
Definition: recordingfile.h:29
#define ENO
This can be appended to the LOG args with "+".
Definition: mythlogging.h:99
int FILE
Definition: mythburn.py:110
#define REPLEX_DVD
Definition: multiplex.h:39
void ApplyTranscoderProfileChange(const QString &profile) const
Sets the transcoder profile for a recording.
#define MYTH_APPNAME_MYTHTRANSCODE
#define REENCODE_CUTLIST_CHANGE
Definition: transcodedefs.h:5
bool prepare(const QString &query)
QSqlQuery::prepare() is not thread safe in Qt <= 3.3.2.
Definition: mythdbcon.cpp:808
static bool ChangeJobArgs(int jobID, const QString &args="")
Definition: jobqueue.cpp:1038
static void UpdatePositionMap(frm_pos_map_t &posMap, frm_pos_map_t &durMap, const QString &mapfile, ProgramInfo *pginfo)
QStringList toStringList(const QString &key, const QString &sep="") const
Returns stored QVariant as a QStringList, falling to default if not provided.
#define LOG(_MASK_, _LEVEL_, _STRING_)
Definition: mythlogging.h:41
#define REPLEX_MPEG2
Definition: multiplex.h:38
static void UpdateJobQueue(float percent_done)
bool GetBoolSetting(const QString &key, bool defaultval=false)
virtual bool Parse(int argc, const char *const *argv)
Loop through argv and populate arguments with values.
AVContainer m_containerFormat
Definition: recordingfile.h:47
uint GetChanID(void) const
This is the unique key used in the database to locate tuning information.
Definition: programinfo.h:366
QString QueryBasename(void) const
Gets the basename, from the DB if necessary.
static uint64_t ReloadBookmark(ProgramInfo *pginfo)
#define SIGHUP
Definition: compat.h:213
#define MYTH_BINARY_VERSION
Update this whenever the plug-in ABI changes.
Definition: mythversion.h:16
bool Init(bool gui=true, bool promptForBackend=false, bool disableAutoDiscovery=false, bool ignoreDB=false)
static QString GetJobArgs(int jobID)
Definition: jobqueue.cpp:1480
QDateTime GetRecordingStartTime(void) const
Approximate time the recording started.
Definition: programinfo.h:398
static QString cleanup(const QString &str)
static bool ChangeJobStatus(int jobID, int newStatus, const QString &comment="")
Definition: jobqueue.cpp:986
QString GetHostname(void) const
Definition: programinfo.h:415
Default UTC.
Definition: mythdate.h:14
QMap< uint64_t, MarkTypes > frm_dir_map_t
Frame # -> Mark map.
Definition: programtypes.h:81
bool exec(void)
Wrap QSqlQuery::exec() so we can display SQL.
Definition: mythdbcon.cpp:603
#define GENERIC_EXIT_NO_MYTHCONTEXT
No MythContext available.
Definition: exitcodes.h:13
QString GetPlaybackURL(bool checkMaster=false, bool forceCheckLocal=false)
Returns filename or URL to be used to play back this recording.
static int CheckJobQueue()
static void DBError(const QString &where, const MSqlQuery &query)
Definition: mythdb.cpp:179
#define GENERIC_EXIT_INVALID_CMDLINE
Command line parse error.
Definition: exitcodes.h:15
static enum JobStatus GetJobStatus(int jobID)
Definition: jobqueue.cpp:1522
uint toUInt(const QString &key) const
Returns stored QVariant as an unsigned integer, falling to default if not provided.
#define REENCODE_STOPPED
Definition: transcodedefs.h:8
bool QueryCutList(frm_dir_map_t &delMap, bool loadAutosave=false) const
static bool QueueJob(int jobType, uint chanid, const QDateTime &recstartts, const QString &args="", const QString &comment="", QString host="", int flags=0, int status=JOB_QUEUED, QDateTime schedruntime=QDateTime())
Definition: jobqueue.cpp:517
void ClearPositionMap(MarkTypes type) const
int toInt(const QString &key) const
Returns stored QVariant as an integer, falling to default if not provided.
int ConfigureLogging(const QString &mask="general", unsigned int progress=0)
Read in logging options and initialize the logging interface.
#define REPLEX_TS_SD
Definition: multiplex.h:41