00001
00002 #include <fcntl.h>
00003
00004
00005 #include <iostream>
00006 #include <fstream>
00007 #include <cerrno>
00008 using namespace std;
00009
00010
00011 #include <QCoreApplication>
00012 #include <QDir>
00013
00014
00015 #include "exitcodes.h"
00016 #include "programinfo.h"
00017 #include "jobqueue.h"
00018 #include "mythcontext.h"
00019 #include "mythdb.h"
00020 #include "mythversion.h"
00021 #include "mythmiscutil.h"
00022 #include "transcode.h"
00023 #include "mpeg2fix.h"
00024 #include "remotefile.h"
00025 #include "mythtranslation.h"
00026 #include "mythlogging.h"
00027 #include "commandlineparser.h"
00028 #include "recordinginfo.h"
00029
00030 static void CompleteJob(int jobID, ProgramInfo *pginfo, bool useCutlist,
00031 frm_dir_map_t *deleteMap, int &resultCode);
00032
00033 static int glbl_jobID = -1;
00034 static QString recorderOptions = "";
00035
00036 static void UpdatePositionMap(frm_pos_map_t &posMap, QString mapfile,
00037 ProgramInfo *pginfo)
00038 {
00039 if (pginfo && mapfile.isEmpty())
00040 {
00041 pginfo->ClearPositionMap(MARK_KEYFRAME);
00042 pginfo->ClearPositionMap(MARK_GOP_START);
00043 pginfo->SavePositionMap(posMap, MARK_GOP_BYFRAME);
00044 }
00045 else if (!mapfile.isEmpty())
00046 {
00047 FILE *mapfh = fopen(mapfile.toLocal8Bit().constData(), "w");
00048 if (!mapfh)
00049 {
00050 LOG(VB_GENERAL, LOG_ERR, QString("Could not open map file '%1'")
00051 .arg(mapfile) + ENO);
00052 return;
00053 }
00054 frm_pos_map_t::const_iterator it;
00055 fprintf (mapfh, "Type: %d\n", MARK_GOP_BYFRAME);
00056 for (it = posMap.begin(); it != posMap.end(); ++it)
00057 fprintf(mapfh, "%lld %lld\n",
00058 (unsigned long long)it.key(), (unsigned long long)*it);
00059 fclose(mapfh);
00060 }
00061 }
00062
00063 static int BuildKeyframeIndex(MPEG2fixup *m2f, QString &infile,
00064 frm_pos_map_t &posMap, int jobID)
00065 {
00066 if (jobID < 0 || JobQueue::GetJobCmd(jobID) != JOB_STOP)
00067 {
00068 if (jobID >= 0)
00069 JobQueue::ChangeJobComment(jobID,
00070 QString(QObject::tr("Generating Keyframe Index")));
00071 int err = m2f->BuildKeyframeIndex(infile, posMap);
00072 if (err)
00073 return err;
00074 if (jobID >= 0)
00075 JobQueue::ChangeJobComment(jobID,
00076 QString(QObject::tr("Transcode Completed")));
00077 }
00078 return 0;
00079 }
00080
00081 static void UpdateJobQueue(float percent_done)
00082 {
00083 JobQueue::ChangeJobComment(glbl_jobID,
00084 QString("%1% " + QObject::tr("Completed"))
00085 .arg(percent_done, 0, 'f', 1));
00086 }
00087
00088 static int CheckJobQueue()
00089 {
00090 if (JobQueue::GetJobCmd(glbl_jobID) == JOB_STOP)
00091 {
00092 LOG(VB_GENERAL, LOG_NOTICE, "Transcoding stopped by JobQueue");
00093 return 1;
00094 }
00095 return 0;
00096 }
00097
00098 static int QueueTranscodeJob(ProgramInfo *pginfo, QString profile,
00099 QString hostname, bool usecutlist)
00100 {
00101 RecordingInfo recinfo(*pginfo);
00102 if (!profile.isEmpty())
00103 recinfo.ApplyTranscoderProfileChange(profile);
00104
00105 if (JobQueue::QueueJob(JOB_TRANSCODE, pginfo->GetChanID(),
00106 pginfo->GetRecordingStartTime(),
00107 hostname, "", "",
00108 usecutlist ? JOB_USE_CUTLIST : 0))
00109 {
00110 LOG(VB_GENERAL, LOG_NOTICE,
00111 QString("Queued transcode job for chanid %1 @ %2")
00112 .arg(pginfo->GetChanID())
00113 .arg(pginfo->GetRecordingStartTime().toString("yyyyMMddhhmmss")));
00114 return GENERIC_EXIT_OK;
00115 }
00116
00117 LOG(VB_GENERAL, LOG_ERR, QString("Error queuing job for chanid %1 @ %2")
00118 .arg(pginfo->GetChanID())
00119 .arg(pginfo->GetRecordingStartTime().toString("yyyyMMddhhmmss")));
00120 return GENERIC_EXIT_DB_ERROR;
00121 }
00122
00123 namespace
00124 {
00125 void cleanup()
00126 {
00127 delete gContext;
00128 gContext = NULL;
00129
00130 }
00131
00132 class CleanupGuard
00133 {
00134 public:
00135 typedef void (*CleanupFunc)();
00136
00137 public:
00138 CleanupGuard(CleanupFunc cleanFunction) :
00139 m_cleanFunction(cleanFunction) {}
00140
00141 ~CleanupGuard()
00142 {
00143 m_cleanFunction();
00144 }
00145
00146 private:
00147 CleanupFunc m_cleanFunction;
00148 };
00149 }
00150
00151 int main(int argc, char *argv[])
00152 {
00153 uint chanid;
00154 QDateTime starttime;
00155 QString infile, outfile;
00156 QString profilename = QString("autodetect");
00157 QString fifodir = NULL;
00158 int jobID = -1;
00159 int jobType = JOB_NONE;
00160 int otype = REPLEX_MPEG2;
00161 bool useCutlist = false, keyframesonly = false;
00162 bool build_index = false, fifosync = false;
00163 bool mpeg2 = false;
00164 bool fifo_info = false;
00165 bool cleanCut = false;
00166 QMap<QString, QString> settingsOverride;
00167 frm_dir_map_t deleteMap;
00168 frm_pos_map_t posMap;
00169 int AudioTrackNo = -1;
00170
00171 int found_starttime = 0;
00172 int found_chanid = 0;
00173 int found_infile = 0;
00174 int update_index = 1;
00175 int isVideo = 0;
00176 bool passthru = false;
00177
00178 MythTranscodeCommandLineParser cmdline;
00179 if (!cmdline.Parse(argc, argv))
00180 {
00181 cmdline.PrintHelp();
00182 return GENERIC_EXIT_INVALID_CMDLINE;
00183 }
00184
00185 if (cmdline.toBool("showhelp"))
00186 {
00187 cmdline.PrintHelp();
00188 return GENERIC_EXIT_OK;
00189 }
00190
00191 if (cmdline.toBool("showversion"))
00192 {
00193 cmdline.PrintVersion();
00194 return GENERIC_EXIT_OK;
00195 }
00196
00197 QCoreApplication a(argc, argv);
00198 QCoreApplication::setApplicationName(MYTH_APPNAME_MYTHTRANSCODE);
00199
00200 if (cmdline.toBool("outputfile"))
00201 {
00202 outfile = cmdline.toString("outputfile");
00203 update_index = 0;
00204 }
00205
00206 bool showprogress = cmdline.toBool("showprogress");
00207
00208 int retval;
00209 QString mask("general");
00210 bool quiet = (outfile == "-") || showprogress;
00211 if ((retval = cmdline.ConfigureLogging(mask, quiet)) != GENERIC_EXIT_OK)
00212 return retval;
00213
00214 if (cmdline.toBool("starttime"))
00215 {
00216 starttime = cmdline.toDateTime("starttime");
00217 found_starttime = 1;
00218 }
00219 if (cmdline.toBool("chanid"))
00220 {
00221 chanid = cmdline.toUInt("chanid");
00222 found_chanid = 1;
00223 }
00224 if (cmdline.toBool("jobid"))
00225 jobID = cmdline.toInt("jobid");
00226 if (cmdline.toBool("inputfile"))
00227 {
00228 infile = cmdline.toString("inputfile");
00229 found_infile = 1;
00230 }
00231 if (cmdline.toBool("video"))
00232 isVideo = true;
00233 if (cmdline.toBool("profile"))
00234 profilename = cmdline.toString("profile");
00235
00236 if (cmdline.toBool("usecutlist"))
00237 {
00238 useCutlist = true;
00239 if (!cmdline.toString("usecutlist").isEmpty())
00240 {
00241 if (!cmdline.toBool("inputfile") && !cmdline.toBool("hls"))
00242 {
00243 LOG(VB_GENERAL, LOG_CRIT, "External cutlists are only allowed "
00244 "when using the --infile option.");
00245 return GENERIC_EXIT_INVALID_CMDLINE;
00246 }
00247
00248 uint64_t last = 0, start, end;
00249 QStringList cutlist = cmdline.toStringList("usecutlist", " ");
00250 QStringList::iterator it;
00251 for (it = cutlist.begin(); it != cutlist.end(); ++it)
00252 {
00253 QStringList startend =
00254 (*it).split("-", QString::SkipEmptyParts);
00255 if (startend.size() == 2)
00256 {
00257 start = startend.first().toULongLong();
00258 end = startend.last().toULongLong();
00259
00260 if (cmdline.toBool("inversecut"))
00261 {
00262 LOG(VB_GENERAL, LOG_DEBUG,
00263 QString("Cutting section %1-%2.")
00264 .arg(last).arg(start));
00265 deleteMap[start] = MARK_CUT_END;
00266 deleteMap[end] = MARK_CUT_START;
00267 last = end;
00268 }
00269 else
00270 {
00271 LOG(VB_GENERAL, LOG_DEBUG,
00272 QString("Cutting section %1-%2.")
00273 .arg(start).arg(end));
00274 deleteMap[start] = MARK_CUT_START;
00275 deleteMap[end] = MARK_CUT_END;
00276 }
00277 }
00278 }
00279
00280 if (cmdline.toBool("inversecut"))
00281 {
00282 if (deleteMap.contains(0) && (deleteMap[0] == MARK_CUT_END))
00283 deleteMap.remove(0);
00284 else
00285 deleteMap[0] = MARK_CUT_START;
00286 deleteMap[999999999] = MARK_CUT_END;
00287 LOG(VB_GENERAL, LOG_DEBUG,
00288 QString("Cutting section %1-999999999.")
00289 .arg(last));
00290 }
00291
00292
00293 if (deleteMap.count() >= 2)
00294 {
00295 frm_dir_map_t::iterator cur = deleteMap.begin(), prev;
00296 prev = cur++;
00297 while (cur != deleteMap.end())
00298 {
00299 if (prev.value() == cur.value())
00300 {
00301
00302 QString err("Cut %1points found at %3 and %4, with no "
00303 "%2 point in between.");
00304 if (prev.value() == MARK_CUT_END)
00305 err = err.arg("end").arg("start");
00306 else
00307 err = err.arg("start").arg("end");
00308 LOG(VB_GENERAL, LOG_CRIT, "Invalid cutlist defined!");
00309 LOG(VB_GENERAL, LOG_CRIT, err.arg(prev.key())
00310 .arg(cur.key()));
00311 return GENERIC_EXIT_INVALID_CMDLINE;
00312 }
00313 else if ( (prev.value() == MARK_CUT_START) &&
00314 ((cur.key() - prev.key()) < 2) )
00315 {
00316 LOG(VB_GENERAL, LOG_WARNING, QString("Discarding "
00317 "insufficiently long cut: %1-%2")
00318 .arg(prev.key()).arg(cur.key()));
00319 prev = deleteMap.erase(prev);
00320 cur = deleteMap.erase(cur);
00321
00322 if (cur == deleteMap.end())
00323 continue;
00324 }
00325 prev = cur++;
00326 }
00327 }
00328 }
00329 else if (cmdline.toBool("inversecut"))
00330 {
00331 cerr << "Cutlist inversion requires an external cutlist be" << endl
00332 << "provided using the --honorcutlist option." << endl;
00333 return GENERIC_EXIT_INVALID_CMDLINE;
00334 }
00335 }
00336
00337 if (cmdline.toBool("cleancut"))
00338 cleanCut = true;
00339
00340 if (cmdline.toBool("allkeys"))
00341 keyframesonly = true;
00342 if (cmdline.toBool("reindex"))
00343 build_index = true;
00344 if (cmdline.toBool("fifodir"))
00345 fifodir = cmdline.toString("fifodir");
00346 if (cmdline.toBool("fifoinfo"))
00347 fifo_info = true;
00348 if (cmdline.toBool("fifosync"))
00349 fifosync = true;
00350 if (cmdline.toBool("recopt"))
00351 recorderOptions = cmdline.toString("recopt");
00352 if (cmdline.toBool("mpeg2"))
00353 mpeg2 = true;
00354 if (cmdline.toBool("ostream"))
00355 {
00356 if (cmdline.toString("ostream") == "dvd")
00357 otype = REPLEX_DVD;
00358 else if (cmdline.toString("ostream") == "ts")
00359 otype = REPLEX_TS_SD;
00360 else
00361 {
00362 cerr << "Invalid 'ostream' type: "
00363 << cmdline.toString("ostream").toLocal8Bit().constData()
00364 << endl;
00365 return GENERIC_EXIT_INVALID_CMDLINE;
00366 }
00367 }
00368 if (cmdline.toBool("audiotrack"))
00369 AudioTrackNo = cmdline.toInt("audiotrack");
00370 if (cmdline.toBool("passthru"))
00371 passthru = true;
00372
00373 CleanupGuard callCleanup(cleanup);
00374
00375 gContext = new MythContext(MYTH_BINARY_VERSION);
00376 if (!gContext->Init(false))
00377 {
00378 LOG(VB_GENERAL, LOG_ERR, "Failed to init MythContext, exiting.");
00379 return GENERIC_EXIT_NO_MYTHCONTEXT;
00380 }
00381
00382 MythTranslation::load("mythfrontend");
00383
00384 cmdline.ApplySettingsOverride();
00385
00386 if (jobID != -1)
00387 {
00388 if (JobQueue::GetJobInfoFromID(jobID, jobType, chanid, starttime))
00389 {
00390 found_starttime = 1;
00391 found_chanid = 1;
00392 }
00393 else
00394 {
00395 cerr << "mythtranscode: ERROR: Unable to find DB info for "
00396 << "JobQueue ID# " << jobID << endl;
00397 return GENERIC_EXIT_NO_RECORDING_DATA;
00398 }
00399 }
00400
00401 if (((!found_infile && !(found_chanid && found_starttime)) ||
00402 (found_infile && (found_chanid || found_starttime))) &&
00403 (!cmdline.toBool("hls")))
00404 {
00405 cerr << "Must specify -i OR -c AND -s options!" << endl;
00406 return GENERIC_EXIT_INVALID_CMDLINE;
00407 }
00408 if (isVideo && !found_infile && !cmdline.toBool("hls"))
00409 {
00410 cerr << "Must specify --infile to use --video" << endl;
00411 return GENERIC_EXIT_INVALID_CMDLINE;
00412 }
00413 if (jobID >= 0 && (found_infile || build_index))
00414 {
00415 cerr << "Can't specify -j with --buildindex, --video or --infile"
00416 << endl;
00417 return GENERIC_EXIT_INVALID_CMDLINE;
00418 }
00419 if ((jobID >= 0) && build_index)
00420 {
00421 cerr << "Can't specify both -j and --buildindex" << endl;
00422 return GENERIC_EXIT_INVALID_CMDLINE;
00423 }
00424 if (keyframesonly && !fifodir.isEmpty())
00425 {
00426 cerr << "Cannot specify both --fifodir and --allkeys" << endl;
00427 return GENERIC_EXIT_INVALID_CMDLINE;
00428 }
00429 if (fifosync && fifodir.isEmpty())
00430 {
00431 cerr << "Must specify --fifodir to use --fifosync" << endl;
00432 return GENERIC_EXIT_INVALID_CMDLINE;
00433 }
00434 if (fifo_info && !fifodir.isEmpty())
00435 {
00436 cerr << "Cannot specify both --fifodir and --fifoinfo" << endl;
00437 return GENERIC_EXIT_INVALID_CMDLINE;
00438 }
00439 if (cleanCut && fifodir.isEmpty() && !fifo_info)
00440 {
00441 cerr << "Clean cutting works only in fifodir mode" << endl;
00442 return GENERIC_EXIT_INVALID_CMDLINE;
00443 }
00444 if (cleanCut && !useCutlist)
00445 {
00446 cerr << "--cleancut is pointless without --honorcutlist" << endl;
00447 return GENERIC_EXIT_INVALID_CMDLINE;
00448 }
00449
00450 if (fifo_info)
00451 {
00452
00453
00454 fifodir = "DummyFifoPath";
00455 }
00456
00457 if (!MSqlQuery::testDBConnection())
00458 {
00459 LOG(VB_GENERAL, LOG_ERR, "couldn't open db");
00460 return GENERIC_EXIT_DB_ERROR;
00461 }
00462
00463 ProgramInfo *pginfo = NULL;
00464 if (cmdline.toBool("hls"))
00465 {
00466 pginfo = new ProgramInfo();
00467 }
00468 else if (isVideo)
00469 {
00470
00471 QFileInfo inf(infile);
00472 infile = inf.absoluteFilePath();
00473 pginfo = new ProgramInfo(infile);
00474 }
00475 else if (!found_infile)
00476 {
00477 pginfo = new ProgramInfo(chanid, starttime);
00478
00479 if (!pginfo->GetChanID())
00480 {
00481 LOG(VB_GENERAL, LOG_ERR,
00482 QString("Couldn't find recording for chanid %1 @ %2")
00483 .arg(chanid).arg(starttime.toString("yyyyMMddhhmmss")));
00484 delete pginfo;
00485 return GENERIC_EXIT_NO_RECORDING_DATA;
00486 }
00487
00488 infile = pginfo->GetPlaybackURL(false, true);
00489 }
00490 else
00491 {
00492 pginfo = new ProgramInfo(infile);
00493 if (!pginfo->GetChanID())
00494 {
00495 LOG(VB_GENERAL, LOG_ERR,
00496 QString("Couldn't find a recording for filename '%1'")
00497 .arg(infile));
00498 delete pginfo;
00499 return GENERIC_EXIT_NO_RECORDING_DATA;
00500 }
00501 }
00502
00503 if (!pginfo)
00504 {
00505 LOG(VB_GENERAL, LOG_ERR, "No program info found!");
00506 return GENERIC_EXIT_NO_RECORDING_DATA;
00507 }
00508
00509 if (cmdline.toBool("queue"))
00510 {
00511 QString hostname = cmdline.toString("queue");
00512 return QueueTranscodeJob(pginfo, profilename, hostname, useCutlist);
00513 }
00514
00515 if (infile.left(7) == "myth://" && (outfile.isEmpty() || outfile != "-") &&
00516 fifodir.isEmpty() && !cmdline.toBool("hls") && !cmdline.toBool("avf"))
00517 {
00518 LOG(VB_GENERAL, LOG_ERR,
00519 QString("Attempted to transcode %1. Mythtranscode is currently "
00520 "unable to transcode remote files.") .arg(infile));
00521 return GENERIC_EXIT_REMOTE_FILE;
00522 }
00523
00524 if (outfile.isEmpty() && !build_index && fifodir.isEmpty())
00525 outfile = infile + ".tmp";
00526
00527 if (jobID >= 0)
00528 JobQueue::ChangeJobStatus(jobID, JOB_RUNNING);
00529
00530 Transcode *transcode = new Transcode(pginfo);
00531
00532 if (!build_index)
00533 {
00534 if (cmdline.toBool("hlsstreamid"))
00535 LOG(VB_GENERAL, LOG_NOTICE,
00536 QString("Transcoding HTTP Live Stream ID %1")
00537 .arg(cmdline.toInt("hlsstreamid")));
00538 else if (fifodir.isEmpty())
00539 LOG(VB_GENERAL, LOG_NOTICE, QString("Transcoding from %1 to %2")
00540 .arg(infile).arg(outfile));
00541 else
00542 LOG(VB_GENERAL, LOG_NOTICE, QString("Transcoding from %1 to FIFO")
00543 .arg(infile));
00544 }
00545
00546 if (cmdline.toBool("avf"))
00547 {
00548 transcode->SetAVFMode();
00549
00550 if (cmdline.toBool("container"))
00551 transcode->SetCMDContainer(cmdline.toString("container"));
00552 if (cmdline.toBool("acodec"))
00553 transcode->SetCMDAudioCodec(cmdline.toString("acodec"));
00554 if (cmdline.toBool("vcodec"))
00555 transcode->SetCMDVideoCodec(cmdline.toString("vcodec"));
00556 }
00557 else if (cmdline.toBool("hls"))
00558 {
00559 transcode->SetHLSMode();
00560
00561 if (cmdline.toBool("hlsstreamid"))
00562 transcode->SetHLSStreamID(cmdline.toInt("hlsstreamid"));
00563 if (cmdline.toBool("maxsegments"))
00564 transcode->SetHLSMaxSegments(cmdline.toInt("maxsegments"));
00565 if (cmdline.toBool("noaudioonly"))
00566 transcode->DisableAudioOnlyHLS();
00567 }
00568
00569 if (cmdline.toBool("avf") || cmdline.toBool("hls"))
00570 {
00571 if (cmdline.toBool("width"))
00572 transcode->SetCMDWidth(cmdline.toInt("width"));
00573 if (cmdline.toBool("height"))
00574 transcode->SetCMDHeight(cmdline.toInt("height"));
00575 if (cmdline.toBool("bitrate"))
00576 transcode->SetCMDBitrate(cmdline.toInt("bitrate") * 1000);
00577 if (cmdline.toBool("audiobitrate"))
00578 transcode->SetCMDAudioBitrate(cmdline.toInt("audiobitrate") * 1000);
00579 }
00580
00581 if (showprogress)
00582 transcode->ShowProgress(true);
00583 if (!recorderOptions.isEmpty())
00584 transcode->SetRecorderOptions(recorderOptions);
00585 int result = 0;
00586 if ((!mpeg2 && !build_index) || cmdline.toBool("hls"))
00587 {
00588 result = transcode->TranscodeFile(infile, outfile,
00589 profilename, useCutlist,
00590 (fifosync || keyframesonly), jobID,
00591 fifodir, fifo_info, cleanCut, deleteMap,
00592 AudioTrackNo, passthru);
00593 if ((result == REENCODE_OK) && (jobID >= 0))
00594 JobQueue::ChangeJobArgs(jobID, "RENAME_TO_NUV");
00595 }
00596
00597 if (fifo_info)
00598 {
00599 delete transcode;
00600 return GENERIC_EXIT_OK;
00601 }
00602
00603 int exitcode = GENERIC_EXIT_OK;
00604 if ((result == REENCODE_MPEG2TRANS) || mpeg2 || build_index)
00605 {
00606 void (*update_func)(float) = NULL;
00607 int (*check_func)() = NULL;
00608 if (useCutlist && !found_infile)
00609 pginfo->QueryCutList(deleteMap);
00610 if (jobID >= 0)
00611 {
00612 glbl_jobID = jobID;
00613 update_func = &UpdateJobQueue;
00614 check_func = &CheckJobQueue;
00615 }
00616
00617 MPEG2fixup *m2f = new MPEG2fixup(infile, outfile,
00618 &deleteMap, NULL, false, false, 20,
00619 showprogress, otype, update_func,
00620 check_func);
00621
00622 if (build_index)
00623 {
00624 int err = BuildKeyframeIndex(m2f, infile, posMap, jobID);
00625 if (err)
00626 return err;
00627 if (update_index)
00628 UpdatePositionMap(posMap, NULL, pginfo);
00629 else
00630 UpdatePositionMap(posMap, outfile + QString(".map"), pginfo);
00631 }
00632 else
00633 {
00634 result = m2f->Start();
00635 if (result == REENCODE_OK)
00636 {
00637 result = BuildKeyframeIndex(m2f, outfile, posMap, jobID);
00638 if (result == REENCODE_OK)
00639 {
00640 if (update_index)
00641 UpdatePositionMap(posMap, NULL, pginfo);
00642 else
00643 UpdatePositionMap(posMap, outfile + QString(".map"),
00644 pginfo);
00645 }
00646 }
00647 }
00648 delete m2f;
00649 }
00650
00651 if (result == REENCODE_OK)
00652 {
00653 if (jobID >= 0)
00654 JobQueue::ChangeJobStatus(jobID, JOB_STOPPING);
00655 LOG(VB_GENERAL, LOG_NOTICE, QString("%1 %2 done")
00656 .arg(build_index ? "Building Index for" : "Transcoding")
00657 .arg(infile));
00658 }
00659 else if (result == REENCODE_CUTLIST_CHANGE)
00660 {
00661 if (jobID >= 0)
00662 JobQueue::ChangeJobStatus(jobID, JOB_RETRY);
00663 LOG(VB_GENERAL, LOG_NOTICE,
00664 QString("Transcoding %1 aborted because of cutlist update")
00665 .arg(infile));
00666 exitcode = GENERIC_EXIT_RESTART;
00667 }
00668 else if (result == REENCODE_STOPPED)
00669 {
00670 if (jobID >= 0)
00671 JobQueue::ChangeJobStatus(jobID, JOB_ABORTING);
00672 LOG(VB_GENERAL, LOG_NOTICE,
00673 QString("Transcoding %1 stopped because of stop command")
00674 .arg(infile));
00675 exitcode = GENERIC_EXIT_KILLED;
00676 }
00677 else
00678 {
00679 if (jobID >= 0)
00680 JobQueue::ChangeJobStatus(jobID, JOB_ERRORING);
00681 LOG(VB_GENERAL, LOG_ERR, QString("Transcoding %1 failed").arg(infile));
00682 exitcode = result;
00683 }
00684
00685 if (jobID >= 0)
00686 CompleteJob(jobID, pginfo, useCutlist, &deleteMap, exitcode);
00687
00688 transcode->deleteLater();
00689
00690 return exitcode;
00691 }
00692
00693 static int transUnlink(QString filename, ProgramInfo *pginfo)
00694 {
00695 if (pginfo != NULL && !pginfo->GetStorageGroup().isEmpty() &&
00696 !pginfo->GetHostname().isEmpty())
00697 {
00698 QString ip = gCoreContext->GetBackendServerIP(pginfo->GetHostname());
00699 QString port = gCoreContext->GetSettingOnHost("BackendServerPort",
00700 pginfo->GetHostname());
00701 QString basename = filename.section('/', -1);
00702 QString uri = gCoreContext->GenMythURL(ip, port, basename,
00703 pginfo->GetStorageGroup());
00704
00705 LOG(VB_GENERAL, LOG_NOTICE, QString("Requesting delete for file '%1'.")
00706 .arg(uri));
00707 bool ok = RemoteFile::DeleteFile(uri);
00708 if (ok)
00709 return 0;
00710 }
00711
00712 LOG(VB_GENERAL, LOG_NOTICE, QString("Deleting file '%1'.").arg(filename));
00713 return unlink(filename.toLocal8Bit().constData());
00714 }
00715
00716 static uint64_t ComputeNewBookmark(uint64_t oldBookmark,
00717 frm_dir_map_t *deleteMap)
00718 {
00719 if (deleteMap == NULL)
00720 return oldBookmark;
00721
00722 uint64_t subtraction = 0;
00723 uint64_t startOfCutRegion = 0;
00724 frm_dir_map_t delMap = *deleteMap;
00725 bool withinCut = false;
00726 bool firstMark = true;
00727 while (delMap.count() && delMap.begin().key() <= oldBookmark)
00728 {
00729 uint64_t key = delMap.begin().key();
00730 MarkTypes mark = delMap.begin().value();
00731
00732 if (mark == MARK_CUT_START && !withinCut)
00733 {
00734 withinCut = true;
00735 startOfCutRegion = key;
00736 }
00737 else if (mark == MARK_CUT_END && firstMark)
00738 {
00739 subtraction += key;
00740 }
00741 else if (mark == MARK_CUT_END && withinCut)
00742 {
00743 withinCut = false;
00744 subtraction += (key - startOfCutRegion);
00745 }
00746 delMap.remove(key);
00747 firstMark = false;
00748 }
00749 if (withinCut)
00750 subtraction += (oldBookmark - startOfCutRegion);
00751 return oldBookmark - subtraction;
00752 }
00753
00754 static uint64_t ReloadBookmark(ProgramInfo *pginfo)
00755 {
00756 MSqlQuery query(MSqlQuery::InitCon());
00757 uint64_t currentBookmark = 0;
00758 query.prepare("SELECT DISTINCT mark FROM recordedmarkup "
00759 "WHERE chanid = :CHANID "
00760 "AND starttime = :STARTIME "
00761 "AND type = :MARKTYPE ;");
00762 query.bindValue(":CHANID", pginfo->GetChanID());
00763 query.bindValue(":STARTTIME", pginfo->GetRecordingStartTime());
00764 query.bindValue(":MARKTYPE", MARK_BOOKMARK);
00765 if (query.exec() && query.next())
00766 {
00767 currentBookmark = query.value(0).toLongLong();
00768 }
00769 return currentBookmark;
00770 }
00771
00772 static void WaitToDelete(ProgramInfo *pginfo)
00773 {
00774 LOG(VB_GENERAL, LOG_NOTICE,
00775 "Transcode: delete old file: waiting while program is in use.");
00776
00777 bool inUse = true;
00778 MSqlQuery query(MSqlQuery::InitCon());
00779 while (inUse)
00780 {
00781 query.prepare("SELECT count(*) FROM inuseprograms "
00782 "WHERE chanid = :CHANID "
00783 "AND starttime = :STARTTIME "
00784 "AND recusage = 'player' ;");
00785 query.bindValue(":CHANID", pginfo->GetChanID());
00786 query.bindValue(":STARTTIME", pginfo->GetRecordingStartTime());
00787 if (!query.exec() || !query.next())
00788 {
00789 LOG(VB_GENERAL, LOG_ERR,
00790 "Transcode: delete old file: in-use query failed;");
00791 inUse = false;
00792 }
00793 else
00794 {
00795 inUse = (query.value(0).toUInt() != 0);
00796 }
00797
00798 if (inUse)
00799 {
00800 const unsigned kSecondsToWait = 10;
00801 LOG(VB_GENERAL, LOG_NOTICE,
00802 QString("Transcode: program in use, rechecking in %1 seconds.")
00803 .arg(kSecondsToWait));
00804 sleep(kSecondsToWait);
00805 }
00806 }
00807 LOG(VB_GENERAL, LOG_NOTICE, "Transcode: program is no longer in use.");
00808 }
00809
00810 static void CompleteJob(int jobID, ProgramInfo *pginfo, bool useCutlist,
00811 frm_dir_map_t *deleteMap, int &resultCode)
00812 {
00813 int status = JobQueue::GetJobStatus(jobID);
00814
00815 if (!pginfo)
00816 {
00817 JobQueue::ChangeJobStatus(jobID, JOB_ERRORED,
00818 "Job errored, unable to find Program Info for job");
00819 return;
00820 }
00821
00822 WaitToDelete(pginfo);
00823
00824 const QString filename = pginfo->GetPlaybackURL(false, true);
00825 const QByteArray fname = filename.toLocal8Bit();
00826
00827 if (status == JOB_STOPPING)
00828 {
00829
00830
00831 uint64_t previousBookmark =
00832 ComputeNewBookmark(ReloadBookmark(pginfo), deleteMap);
00833 pginfo->SaveBookmark(previousBookmark);
00834
00835 const QString jobArgs = JobQueue::GetJobArgs(jobID);
00836
00837 const QString tmpfile = filename + ".tmp";
00838 const QByteArray atmpfile = tmpfile.toLocal8Bit();
00839
00840
00841 const QString oldfile = filename + ".old";
00842 const QByteArray aoldfile = oldfile.toLocal8Bit();
00843
00844 QFileInfo st(tmpfile);
00845 qint64 newSize = 0;
00846 if (st.exists())
00847 newSize = st.size();
00848
00849 QString cnf = filename;
00850 if ((jobArgs == "RENAME_TO_NUV") &&
00851 (filename.contains(QRegExp("mpg$"))))
00852 {
00853 QString newbase = pginfo->QueryBasename();
00854
00855 cnf.replace(QRegExp("mpg$"), "nuv");
00856 newbase.replace(QRegExp("mpg$"), "nuv");
00857 pginfo->SaveBasename(newbase);
00858 }
00859
00860 const QString newfile = cnf;
00861 const QByteArray anewfile = newfile.toLocal8Bit();
00862
00863 if (rename(fname.constData(), aoldfile.constData()) == -1)
00864 {
00865 LOG(VB_GENERAL, LOG_ERR,
00866 QString("mythtranscode: Error Renaming '%1' to '%2'")
00867 .arg(filename).arg(oldfile) + ENO);
00868 }
00869
00870 if (rename(atmpfile.constData(), anewfile.constData()) == -1)
00871 {
00872 LOG(VB_GENERAL, LOG_ERR,
00873 QString("mythtranscode: Error Renaming '%1' to '%2'")
00874 .arg(tmpfile).arg(newfile) + ENO);
00875 }
00876
00877 if (!gCoreContext->GetNumSetting("SaveTranscoding", 0))
00878 {
00879 int err;
00880 bool followLinks =
00881 gCoreContext->GetNumSetting("DeletesFollowLinks", 0);
00882
00883 LOG(VB_FILE, LOG_INFO,
00884 QString("mythtranscode: About to unlink/delete file: %1")
00885 .arg(oldfile));
00886
00887 QFileInfo finfo(oldfile);
00888 if (followLinks && finfo.isSymLink())
00889 {
00890 QString link = getSymlinkTarget(oldfile);
00891 QByteArray alink = link.toLocal8Bit();
00892 err = transUnlink(alink.constData(), pginfo);
00893
00894 if (err)
00895 {
00896 LOG(VB_GENERAL, LOG_ERR,
00897 QString("mythtranscode: Error deleting '%1' "
00898 "pointed to by '%2'")
00899 .arg(alink.constData())
00900 .arg(aoldfile.constData()) + ENO);
00901 }
00902
00903 err = unlink(aoldfile.constData());
00904 if (err)
00905 {
00906 LOG(VB_GENERAL, LOG_ERR,
00907 QString("mythtranscode: Error deleting '%1', "
00908 "a link pointing to '%2'")
00909 .arg(aoldfile.constData())
00910 .arg(alink.constData()) + ENO);
00911 }
00912 }
00913 else
00914 {
00915 if ((err = transUnlink(aoldfile.constData(), pginfo)))
00916 LOG(VB_GENERAL, LOG_ERR,
00917 QString("mythtranscode: Error deleting '%1': ")
00918 .arg(oldfile) + ENO);
00919 }
00920 }
00921
00922
00923
00924
00925
00926 if (useCutlist)
00927 {
00928 QFileInfo fInfo(filename);
00929 QString nameFilter = fInfo.fileName() + "*.png";
00930
00931
00932
00933 nameFilter.replace(QRegExp("( |;)"), "?");
00934 QDir dir (fInfo.path(), nameFilter);
00935
00936 for (uint nIdx = 0; nIdx < dir.count(); nIdx++)
00937 {
00938
00939
00940
00941 const QString oldfileop = QString("%1/%2")
00942 .arg(fInfo.path()).arg(dir[nIdx]);
00943 const QByteArray aoldfileop = oldfileop.toLocal8Bit();
00944 transUnlink(aoldfileop.constData(), pginfo);
00945 }
00946 }
00947
00948
00949 if (jobArgs == "RENAME_TO_NUV")
00950 {
00951 QFileInfo fInfo(filename);
00952 QString nameFilter = fInfo.fileName() + "*.png";
00953
00954
00955
00956 nameFilter.replace(QRegExp("( |;)"), "?");
00957
00958 QDir dir (fInfo.path(), nameFilter);
00959
00960 for (uint nIdx = 0; nIdx < dir.count(); nIdx++)
00961 {
00962 const QString oldfileprev = QString("%1/%2")
00963 .arg(fInfo.path()).arg(dir[nIdx]);
00964 const QByteArray aoldfileprev = oldfileprev.toLocal8Bit();
00965
00966 QString newfileprev = oldfileprev;
00967 QRegExp re("mpg(\\..*)?\\.png$");
00968 if (re.indexIn(newfileprev))
00969 {
00970 newfileprev.replace(
00971 re, QString("nuv%1.png").arg(re.cap(1)));
00972 }
00973 const QByteArray anewfileprev = newfileprev.toLocal8Bit();
00974
00975 QFile checkFile(oldfileprev);
00976
00977 if ((oldfileprev != newfileprev) && (checkFile.exists()))
00978 rename(aoldfileprev.constData(), anewfileprev.constData());
00979 }
00980 }
00981
00982 MSqlQuery query(MSqlQuery::InitCon());
00983
00984 if (useCutlist)
00985 {
00986 query.prepare("DELETE FROM recordedmarkup "
00987 "WHERE chanid = :CHANID "
00988 "AND starttime = :STARTTIME "
00989 "AND type != :BOOKMARK ");
00990 query.bindValue(":CHANID", pginfo->GetChanID());
00991 query.bindValue(":STARTTIME", pginfo->GetRecordingStartTime());
00992 query.bindValue(":BOOKMARK", MARK_BOOKMARK);
00993
00994 if (!query.exec())
00995 MythDB::DBError("Error in mythtranscode", query);
00996
00997 query.prepare("UPDATE recorded "
00998 "SET cutlist = :CUTLIST "
00999 "WHERE chanid = :CHANID "
01000 "AND starttime = :STARTTIME ;");
01001 query.bindValue(":CUTLIST", "0");
01002 query.bindValue(":CHANID", pginfo->GetChanID());
01003 query.bindValue(":STARTTIME", pginfo->GetRecordingStartTime());
01004
01005 if (!query.exec())
01006 MythDB::DBError("Error in mythtranscode", query);
01007
01008 pginfo->SaveCommFlagged(COMM_FLAG_NOT_FLAGGED);
01009 }
01010 else
01011 {
01012 query.prepare("DELETE FROM recordedmarkup "
01013 "WHERE chanid = :CHANID "
01014 "AND starttime = :STARTTIME "
01015 "AND type not in ( :COMM_START, "
01016 " :COMM_END, :BOOKMARK, "
01017 " :CUTLIST_START, :CUTLIST_END) ;");
01018 query.bindValue(":CHANID", pginfo->GetChanID());
01019 query.bindValue(":STARTTIME", pginfo->GetRecordingStartTime());
01020 query.bindValue(":COMM_START", MARK_COMM_START);
01021 query.bindValue(":COMM_END", MARK_COMM_END);
01022 query.bindValue(":BOOKMARK", MARK_BOOKMARK);
01023 query.bindValue(":CUTLIST_START", MARK_CUT_START);
01024 query.bindValue(":CUTLIST_END", MARK_CUT_END);
01025
01026 if (!query.exec())
01027 MythDB::DBError("Error in mythtranscode", query);
01028 }
01029
01030 if (newSize)
01031 pginfo->SaveFilesize(newSize);
01032
01033 JobQueue::ChangeJobStatus(jobID, JOB_FINISHED);
01034
01035 } else {
01036
01037 QString filename_tmp = filename + ".tmp";
01038 QByteArray fname_tmp = filename_tmp.toLocal8Bit();
01039 LOG(VB_GENERAL, LOG_NOTICE, QString("Deleting %1").arg(filename_tmp));
01040 transUnlink(fname_tmp.constData(), pginfo);
01041
01042 QString filename_map = filename + ".tmp.map";
01043 QByteArray fname_map = filename_map.toLocal8Bit();
01044 unlink(fname_map.constData());
01045
01046 if (status == JOB_ABORTING)
01047 JobQueue::ChangeJobStatus(jobID, JOB_ABORTED, "Job Aborted");
01048 else if (status != JOB_ERRORING)
01049 resultCode = GENERIC_EXIT_RESTART;
01050 else
01051 JobQueue::ChangeJobStatus(jobID, JOB_ERRORED,
01052 "Unrecoverable error");
01053 }
01054 }
01055