|
MythTV
0.26-pre
|
00001 // POSIX headers 00002 #include <fcntl.h> // for open flags 00003 00004 // C++ headers 00005 #include <iostream> 00006 #include <fstream> 00007 #include <cerrno> 00008 using namespace std; 00009 00010 // Qt headers 00011 #include <QCoreApplication> 00012 #include <QDir> 00013 00014 // MythTV headers 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 // sanitize cutlist 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 // two of the same type next to each other 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 // Load the context 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 // Setup a dummy fifodir path, so that the "fifodir" code path 00453 // is taken. The path wont actually be used. 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 // We want the absolute file path for the filemarkup table 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 // Transcoding may take several minutes. Reload the bookmark 00830 // in case it changed, then save its translated value back. 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 // To save the original file... 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 // Delete previews if cutlist was applied. They will be re-created as 00923 // required. This prevents the user from being stuck with a preview 00924 // from a cut area and ensures that the "dimensioned" previews 00925 // correspond to the new timeline 00926 if (useCutlist) 00927 { 00928 QFileInfo fInfo(filename); 00929 QString nameFilter = fInfo.fileName() + "*.png"; 00930 // QDir's nameFilter uses spaces or semicolons to separate globs, 00931 // so replace them with the "match any character" wildcard 00932 // since mythrename.pl may have included them in filenames 00933 nameFilter.replace(QRegExp("( |;)"), "?"); 00934 QDir dir (fInfo.path(), nameFilter); 00935 00936 for (uint nIdx = 0; nIdx < dir.count(); nIdx++) 00937 { 00938 // If unlink fails, keeping the old preview is not a problem. 00939 // The RENAME_TO_NUV check below will attempt to rename the 00940 // file, if required. 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 /* Rename all preview thumbnails. */ 00949 if (jobArgs == "RENAME_TO_NUV") 00950 { 00951 QFileInfo fInfo(filename); 00952 QString nameFilter = fInfo.fileName() + "*.png"; 00953 // QDir's nameFilter uses spaces or semicolons to separate globs, 00954 // so replace them with the "match any character" wildcard 00955 // since mythrename.pl may have included them in filenames 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 // Not a successful run, so remove the files we created 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) // Stop command was sent 01047 JobQueue::ChangeJobStatus(jobID, JOB_ABORTED, "Job Aborted"); 01048 else if (status != JOB_ERRORING) // Recoverable error 01049 resultCode = GENERIC_EXIT_RESTART; 01050 else // Unrecoverable error 01051 JobQueue::ChangeJobStatus(jobID, JOB_ERRORED, 01052 "Unrecoverable error"); 01053 } 01054 } 01055 /* vim: set expandtab tabstop=4 shiftwidth=4: */
1.7.6.1