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