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