File indexing completed on 2025-03-16 04:29:32
0001 /* 0002 SPDX-FileCopyrightText: 2006-2009 Sebastian Trueg <trueg@k3b.org> 0003 SPDX-FileCopyrightText: 2009 Michal Malek <michalm@jabster.pl> 0004 SPDX-FileCopyrightText: 1998-2009 Sebastian Trueg <trueg@k3b.org> 0005 0006 SPDX-License-Identifier: GPL-2.0-or-later 0007 */ 0008 0009 #include "k3bvideodvdtitletranscodingjob.h" 0010 0011 #include "k3bexternalbinmanager.h" 0012 #include "k3bprocess.h" 0013 #include "k3bcore.h" 0014 #include "k3bglobals.h" 0015 #include "k3bmediacache.h" 0016 #include "k3bmedium.h" 0017 #include "k3b_i18n.h" 0018 0019 #include <QDebug> 0020 #include <QDir> 0021 #include <QFile> 0022 #include <QFileInfo> 0023 0024 0025 class K3b::VideoDVDTitleTranscodingJob::Private 0026 { 0027 public: 0028 const K3b::ExternalBin* usedTranscodeBin; 0029 0030 K3b::Process* process; 0031 0032 QString twoPassEncodingLogFile; 0033 0034 int currentEncodingPass; 0035 0036 bool canceled; 0037 0038 int lastProgress; 0039 int lastSubProgress; 0040 0041 bool getEncodedFrames( const QString& line, int& encodedFrames ) const; 0042 }; 0043 0044 0045 bool K3b::VideoDVDTitleTranscodingJob::Private::getEncodedFrames( const QString& line, int& encodedFrames ) const 0046 { 0047 int pos1 = 0; 0048 int pos2 = 0; 0049 0050 if ( usedTranscodeBin->version() >= Version( 1, 1, 0 ) ) { 0051 // encoding=1 frame=1491 first=0 last=-1 fps=14.815 done=-1.000000 timestamp=59.640 timeleft=-1 decodebuf=12 filterbuf=5 encodebuf=3 0052 if( line.startsWith( "encoding=" ) ) { 0053 pos1 = line.indexOf( '=', 9 ); 0054 pos2 = line.indexOf( ' ', pos1+1 ); 0055 } 0056 } 0057 else { 0058 // encoding frames [000000-000144], 27.58 fps, EMT: 0:00:05, ( 0| 0| 0) 0059 if( line.startsWith( "encoding frame" ) ) { 0060 pos1 = line.indexOf( '-', 15 ); 0061 pos2 = line.indexOf( ']', pos1+1 ); 0062 } 0063 } 0064 0065 if( pos1 > 0 && pos2 > 0 ) { 0066 bool ok; 0067 encodedFrames = line.mid( pos1+1, pos2-pos1-1 ).toInt( &ok ); 0068 return ok; 0069 } 0070 else { 0071 return false; 0072 } 0073 } 0074 0075 0076 K3b::VideoDVDTitleTranscodingJob::VideoDVDTitleTranscodingJob( K3b::JobHandler* hdl, QObject* parent ) 0077 : K3b::Job( hdl, parent ), 0078 m_clippingTop( 0 ), 0079 m_clippingBottom( 0 ), 0080 m_clippingLeft( 0 ), 0081 m_clippingRight( 0 ), 0082 m_width( 0 ), 0083 m_height( 0 ), 0084 m_titleNumber( 1 ), 0085 m_audioStreamIndex( 0 ), 0086 m_videoCodec( VIDEO_CODEC_FFMPEG_MPEG4 ), 0087 m_audioCodec( AUDIO_CODEC_MP3 ), 0088 m_videoBitrate( 1800 ), 0089 m_audioBitrate( 128 ), 0090 m_audioVBR( false ), 0091 m_resampleAudio( false ), 0092 m_twoPassEncoding( false ), 0093 m_lowPriority( true ) 0094 { 0095 d = new Private; 0096 d->process = 0; 0097 } 0098 0099 0100 K3b::VideoDVDTitleTranscodingJob::~VideoDVDTitleTranscodingJob() 0101 { 0102 if( d->process ) { 0103 disconnect( d->process ); 0104 d->process->deleteLater(); 0105 } 0106 delete d; 0107 } 0108 0109 0110 void K3b::VideoDVDTitleTranscodingJob::start() 0111 { 0112 jobStarted(); 0113 0114 d->canceled = false; 0115 d->lastProgress = 0; 0116 0117 d->usedTranscodeBin = k3bcore->externalBinManager()->binObject("transcode"); 0118 if( !d->usedTranscodeBin ) { 0119 emit infoMessage( i18n("%1 executable could not be found.",QString("transcode")), MessageError ); 0120 jobFinished( false ); 0121 return; 0122 } 0123 0124 if( d->usedTranscodeBin->version() < K3b::Version( 1, 0, 0 ) ){ 0125 emit infoMessage( i18n("%1 version %2 is too old." 0126 ,QString("transcode") 0127 ,d->usedTranscodeBin->version()), MessageError ); 0128 jobFinished( false ); 0129 return; 0130 } 0131 0132 emit debuggingOutput( QLatin1String( "Used versions" ), QString::fromLatin1( "transcode: %1" ).arg(d->usedTranscodeBin->version()) ); 0133 0134 if( !d->usedTranscodeBin->copyright().isEmpty() ) 0135 emit infoMessage( i18n("Using %1 %2 – Copyright © %3" 0136 ,d->usedTranscodeBin->name() 0137 ,d->usedTranscodeBin->version() 0138 ,d->usedTranscodeBin->copyright()), MessageInfo ); 0139 0140 // 0141 // Let's take a look at the filename 0142 // 0143 if( m_filename.isEmpty() ) { 0144 m_filename = K3b::findTempFile( "avi" ); 0145 } 0146 else { 0147 // let's see if the directory exists and we can write to it 0148 QFileInfo fileInfo( m_filename ); 0149 QFileInfo dirInfo( fileInfo.path() ); 0150 if( !dirInfo.exists() && !QDir().mkpath( dirInfo.absoluteFilePath() ) ) { 0151 emit infoMessage( i18n("Unable to create folder '%1'",dirInfo.filePath()), MessageError ); 0152 return; 0153 } 0154 else { 0155 dirInfo.refresh(); 0156 if( !dirInfo.isDir() || !dirInfo.isWritable() ) { 0157 emit infoMessage( i18n("Invalid filename: '%1'",m_filename), MessageError ); 0158 jobFinished( false ); 0159 return; 0160 } 0161 } 0162 } 0163 0164 // 0165 // Determine a log file for two-pass encoding 0166 // 0167 d->twoPassEncodingLogFile = K3b::findTempFile( "log" ); 0168 0169 emit newTask( i18n("Transcoding title %1 from Video DVD %2", m_titleNumber, k3bcore->mediaCache()->medium( m_dvd.device() ).beautifiedVolumeId()) ); 0170 0171 // 0172 // Ok then, let's begin 0173 // 0174 startTranscode( m_twoPassEncoding ? 1 : 0 ); 0175 } 0176 0177 0178 void K3b::VideoDVDTitleTranscodingJob::startTranscode( int pass ) 0179 { 0180 d->currentEncodingPass = pass; 0181 d->lastSubProgress = 0; 0182 0183 QString videoCodecString; 0184 switch( m_videoCodec ) { 0185 case VIDEO_CODEC_XVID: 0186 videoCodecString = "xvid"; 0187 break; 0188 0189 case VIDEO_CODEC_FFMPEG_MPEG4: 0190 videoCodecString = "ffmpeg"; 0191 break; 0192 0193 default: 0194 emit infoMessage( i18n("Invalid video codec set: %1",m_videoCodec), MessageError ); 0195 jobFinished( false ); 0196 return; 0197 } 0198 0199 QString audioCodecString; 0200 switch( m_audioCodec ) { 0201 case AUDIO_CODEC_MP3: 0202 audioCodecString = "0x55"; 0203 break; 0204 0205 // ogg only works (as in: transcode does something) with .y <codec>,ogg 0206 // but then the video is garbage (at least to xine and mplayer on my system) 0207 // case AUDIO_CODEC_OGG_VORBIS: 0208 // audioCodecString = "0xfffe"; 0209 // break; 0210 0211 case AUDIO_CODEC_AC3_STEREO: 0212 case AUDIO_CODEC_AC3_PASSTHROUGH: 0213 audioCodecString = "0x2000"; 0214 break; 0215 0216 default: 0217 emit infoMessage( i18n("Invalid audio codec set: %1",m_audioCodec), MessageError ); 0218 jobFinished( false ); 0219 return; 0220 } 0221 0222 // 0223 // prepare the process 0224 // 0225 if( d->process ) { 0226 disconnect( d->process ); 0227 d->process->deleteLater(); 0228 } 0229 d->process = new K3b::Process(); 0230 d->process->setSuppressEmptyLines(true); 0231 d->process->setSplitStdout(true); 0232 connect( d->process, SIGNAL(stdoutLine(QString)), this, SLOT(slotTranscodeStderr(QString)) ); 0233 connect( d->process, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(slotTranscodeExited(int,QProcess::ExitStatus)) ); 0234 0235 // the executable 0236 *d->process << d->usedTranscodeBin; 0237 0238 // low priority 0239 if( m_lowPriority ) 0240 *d->process << "--nice" << "19"; 0241 0242 if ( d->usedTranscodeBin->version() >= Version( 1, 1, 0 ) ) 0243 *d->process << "--log_no_color"; 0244 0245 // we only need 100 steps, but to make sure we use 150 0246 int progressRate = qMax( 1, ( int )m_dvd[m_titleNumber-1].playbackTime().totalFrames()/150 ); 0247 if ( d->usedTranscodeBin->version().simplify() >= K3b::Version( 1, 1, 0 ) ) 0248 *d->process << "--progress_meter" << "2" << "--progress_rate" << QString::number(progressRate); 0249 else 0250 *d->process << "--print_status" << QString::number(progressRate); 0251 0252 // the input 0253 *d->process << "-i" << m_dvd.device()->blockDeviceName(); 0254 0255 // just to make sure 0256 *d->process << "-x" << "dvd"; 0257 0258 // select the title number 0259 *d->process << "-T" << QString("%1,-1,1").arg( m_titleNumber ); 0260 0261 // select the audio stream to extract 0262 if ( m_dvd[m_titleNumber-1].numAudioStreams() > 0 ) 0263 *d->process << "-a" << QString::number( m_audioStreamIndex ); 0264 0265 // clipping 0266 *d->process << "-j" << QString("%1,%2,%3,%4") 0267 .arg(m_clippingTop) 0268 .arg(m_clippingLeft) 0269 .arg(m_clippingBottom) 0270 .arg(m_clippingRight); 0271 0272 // select the encoding type (single pass or two-pass) and the log file for two-pass encoding 0273 // the latter is unused for pass = 0 0274 *d->process << "-R" << QString("%1,%2").arg( pass ).arg( d->twoPassEncodingLogFile ); 0275 0276 // depending on the pass we use different options 0277 if( pass != 1 ) { 0278 // select video codec 0279 *d->process << "-y" << videoCodecString; 0280 0281 // select the audio codec to use 0282 *d->process << "-N" << audioCodecString; 0283 0284 if( m_audioCodec == AUDIO_CODEC_AC3_PASSTHROUGH ) { 0285 // keep 5.1 sound 0286 *d->process << "-A"; 0287 } 0288 else { 0289 // audio quality settings 0290 *d->process << "-b" << QString("%1,%2").arg(m_audioBitrate).arg(m_audioVBR ? 1 : 0); 0291 0292 // resample audio stream to 44.1 khz 0293 if( m_resampleAudio ) 0294 *d->process << "-E" << "44100"; 0295 } 0296 0297 // the output filename 0298 *d->process << "-o" << m_filename; 0299 } 0300 else { 0301 // gather information about the video stream, ignore audio 0302 *d->process << "-y" << QString("%1,null").arg( videoCodecString ); 0303 0304 // we ignore the output from the first pass 0305 *d->process << "-o" << "/dev/null"; 0306 } 0307 0308 // choose the ffmpeg codec 0309 if( m_videoCodec == VIDEO_CODEC_FFMPEG_MPEG4 ) { 0310 *d->process << "-F" << "mpeg4"; 0311 } 0312 0313 // video bitrate 0314 *d->process << "-w" << QString::number( m_videoBitrate ); 0315 0316 // video resizing 0317 int usedWidth = m_width; 0318 int usedHeight = m_height; 0319 if( m_width == 0 || m_height == 0 ) { 0320 // 0321 // The "real" size of the video, considering anamorph encoding 0322 // 0323 int realHeight = m_dvd[m_titleNumber-1].videoStream().realPictureHeight(); 0324 int readWidth = m_dvd[m_titleNumber-1].videoStream().realPictureWidth(); 0325 0326 // 0327 // The clipped size with the correct aspect ratio 0328 // 0329 int clippedHeight = realHeight - m_clippingTop - m_clippingBottom; 0330 int clippedWidth = readWidth - m_clippingLeft - m_clippingRight; 0331 0332 // 0333 // Now simply resize the clipped video to the wanted size 0334 // 0335 if( usedWidth > 0 ) { 0336 usedHeight = clippedHeight * usedWidth / clippedWidth; 0337 } 0338 else { 0339 if( usedHeight == 0 ) { 0340 // 0341 // This is the default case in which both m_width and m_height are 0. 0342 // The result will be a size of clippedWidth x clippedHeight 0343 // 0344 usedHeight = clippedHeight; 0345 } 0346 usedWidth = clippedWidth * usedHeight / clippedHeight; 0347 } 0348 } 0349 0350 // 0351 // Now make sure both width and height are multiple of 16 the simple way 0352 // 0353 usedWidth -= usedWidth%16; 0354 usedHeight -= usedHeight%16; 0355 0356 // we only give information about the resizing of the video once 0357 if( pass < 2 ) 0358 emit infoMessage( i18n("Resizing picture of title %1 to %2x%3",m_titleNumber,usedWidth,usedHeight), MessageInfo ); 0359 *d->process << "-Z" << QString("%1x%2").arg(usedWidth).arg(usedHeight); 0360 0361 // additional user parameters from config 0362 const QStringList& params = d->usedTranscodeBin->userParameters(); 0363 for( QStringList::const_iterator it = params.begin(); it != params.end(); ++it ) 0364 *d->process << *it; 0365 0366 // produce some debugging output 0367 qDebug() << "***** transcode parameters:\n"; 0368 QString s = d->process->joinedArgs(); 0369 qDebug() << s << Qt::flush; 0370 emit debuggingOutput( d->usedTranscodeBin->name() + " command:", s); 0371 0372 // start the process 0373 if( !d->process->start( KProcess::MergedChannels ) ) { 0374 // something went wrong when starting the program 0375 // it "should" be the executable 0376 emit infoMessage( i18n("Could not start %1.",d->usedTranscodeBin->name()), K3b::Job::MessageError ); 0377 jobFinished(false); 0378 } 0379 else { 0380 if( pass == 0 ) 0381 emit newSubTask( i18n("Single-pass Encoding") ); 0382 else if( pass == 1 ) 0383 emit newSubTask( i18n("Two-pass Encoding: First Pass") ); 0384 else 0385 emit newSubTask( i18n("Two-pass Encoding: Second Pass") ); 0386 0387 emit subPercent( 0 ); 0388 } 0389 } 0390 0391 0392 void K3b::VideoDVDTitleTranscodingJob::cancel() 0393 { 0394 // FIXME: do not cancel before one frame has been encoded. transcode seems to hang then 0395 // find a way to determine all subprocess ids to kill all of them 0396 d->canceled = true; 0397 if( d->process && d->process->isRunning() ) 0398 d->process->kill(); 0399 } 0400 0401 0402 void K3b::VideoDVDTitleTranscodingJob::cleanup( bool success ) 0403 { 0404 if( QFile::exists( d->twoPassEncodingLogFile ) ) { 0405 QFile::remove( d->twoPassEncodingLogFile ); 0406 } 0407 0408 if( !success && QFile::exists( m_filename ) ) { 0409 emit infoMessage( i18n("Removing incomplete video file '%1'",m_filename), MessageInfo ); 0410 QFile::remove( m_filename ); 0411 } 0412 } 0413 0414 0415 void K3b::VideoDVDTitleTranscodingJob::slotTranscodeStderr( const QString& line ) 0416 { 0417 emit debuggingOutput( "transcode", line ); 0418 0419 int encodedFrames; 0420 0421 // parse progress 0422 if( d->getEncodedFrames( line, encodedFrames ) ) { 0423 int totalFrames = m_dvd[m_titleNumber-1].playbackTime().totalFrames(); 0424 if( totalFrames > 0 ) { 0425 int progress = 100 * encodedFrames / totalFrames; 0426 0427 if( progress > d->lastSubProgress ) { 0428 d->lastSubProgress = progress; 0429 emit subPercent( progress ); 0430 } 0431 0432 if( m_twoPassEncoding ) { 0433 progress /= 2; 0434 if( d->currentEncodingPass == 2 ) 0435 progress += 50; 0436 } 0437 0438 if( progress > d->lastProgress ) { 0439 d->lastProgress = progress; 0440 emit percent( progress ); 0441 } 0442 } 0443 } 0444 } 0445 0446 0447 void K3b::VideoDVDTitleTranscodingJob::slotTranscodeExited( int exitCode, QProcess::ExitStatus exitStatus ) 0448 { 0449 if( d->canceled ) { 0450 emit canceled(); 0451 cleanup( false ); 0452 jobFinished( false ); 0453 } 0454 else if( exitStatus == QProcess::NormalExit ) { 0455 switch( exitCode ) { 0456 case 0: 0457 if( d->currentEncodingPass == 1 ) { 0458 emit percent( 50 ); 0459 // start second encoding pass 0460 startTranscode( 2 ); 0461 } 0462 else { 0463 emit percent( 100 ); 0464 cleanup( true ); 0465 jobFinished( true ); 0466 } 0467 break; 0468 0469 default: 0470 // FIXME: error handling 0471 0472 emit infoMessage( i18n("%1 returned an unknown error (code %2).", 0473 d->usedTranscodeBin->name(), exitCode ), 0474 K3b::Job::MessageError ); 0475 emit infoMessage( i18n("Please send me an email with the last output."), K3b::Job::MessageError ); 0476 0477 cleanup( false ); 0478 jobFinished( false ); 0479 } 0480 } 0481 else { 0482 cleanup( false ); 0483 emit infoMessage( i18n("Execution of %1 failed.",QString("transcode")), MessageError ); 0484 emit infoMessage( i18n("Please consult the debugging output for details."), MessageError ); 0485 jobFinished( false ); 0486 } 0487 } 0488 0489 0490 void K3b::VideoDVDTitleTranscodingJob::setClipping( int top, int left, int bottom, int right ) 0491 { 0492 m_clippingTop = top; 0493 m_clippingLeft = left; 0494 m_clippingBottom = bottom; 0495 m_clippingRight = right; 0496 0497 // 0498 // transcode seems unable to handle different clipping values for left and right 0499 // 0500 m_clippingLeft = m_clippingRight = qMin( m_clippingRight, m_clippingLeft ); 0501 } 0502 0503 0504 void K3b::VideoDVDTitleTranscodingJob::setSize( int width, int height ) 0505 { 0506 m_width = width; 0507 m_height = height; 0508 } 0509 0510 0511 QString K3b::VideoDVDTitleTranscodingJob::audioCodecString( K3b::VideoDVDTitleTranscodingJob::AudioCodec codec ) 0512 { 0513 switch( codec ) { 0514 case AUDIO_CODEC_AC3_STEREO: 0515 return i18n("AC3 (Stereo)"); 0516 case AUDIO_CODEC_AC3_PASSTHROUGH: 0517 return i18n("AC3 (Pass-through)"); 0518 case AUDIO_CODEC_MP3: 0519 return i18n("MPEG1 Layer III"); 0520 default: 0521 return "unknown audio codec"; 0522 } 0523 } 0524 0525 0526 QString K3b::VideoDVDTitleTranscodingJob::videoCodecString( K3b::VideoDVDTitleTranscodingJob::VideoCodec codec ) 0527 { 0528 switch( codec ) { 0529 case VIDEO_CODEC_FFMPEG_MPEG4: 0530 return i18n("MPEG4 (FFMPEG)"); 0531 case VIDEO_CODEC_XVID: 0532 return i18n("XviD"); 0533 default: 0534 return "unknown video codec"; 0535 } 0536 } 0537 0538 0539 QString K3b::VideoDVDTitleTranscodingJob::videoCodecDescription( K3b::VideoDVDTitleTranscodingJob::VideoCodec codec ) 0540 { 0541 switch( codec ) { 0542 case VIDEO_CODEC_FFMPEG_MPEG4: 0543 return i18n("FFmpeg is an open-source project trying to support most video and audio codecs used " 0544 "these days. Its subproject libavcodec forms the basis for multimedia players such as " 0545 "xine or mplayer.") 0546 + "<br>" 0547 + i18n("FFmpeg contains an implementation of the MPEG-4 video encoding standard which produces " 0548 "high quality results."); 0549 case VIDEO_CODEC_XVID: 0550 return i18n("XviD is a free and open source MPEG-4 video codec. XviD was created by a group of " 0551 "volunteer programmers after the OpenDivX source was closed in July 2001.") 0552 + "<br>" 0553 + i18n("XviD features MPEG-4 Advanced Profile settings such as b-frames, global " 0554 "and quarter pixel motion compensation, lumi masking, trellis quantization, and " 0555 "H.263, MPEG and custom quantization matrices.") 0556 + "<br>" 0557 + i18n("XviD is a primary competitor of DivX (XviD being DivX spelled backwards). " 0558 "While DivX is closed source and may only run on Windows, Mac OS and Linux, " 0559 "XviD is open source and can potentially run on any platform.") 0560 + "<br><em>" 0561 + i18n("(Description taken from the Wikipedia article)") 0562 + "</em>"; 0563 default: 0564 return "unknown video codec"; 0565 } 0566 } 0567 0568 0569 QString K3b::VideoDVDTitleTranscodingJob::audioCodecDescription( K3b::VideoDVDTitleTranscodingJob::AudioCodec codec ) 0570 { 0571 static QString s_ac3General = i18n("AC3, better known as Dolby Digital is standardized as ATSC A/52. " 0572 "It contains up to 6 total channels of sound."); 0573 switch( codec ) { 0574 case AUDIO_CODEC_AC3_STEREO: 0575 return s_ac3General 0576 + "<br>" + i18n("With this setting K3b will create a two-channel stereo " 0577 "Dolby Digital audio stream."); 0578 case AUDIO_CODEC_AC3_PASSTHROUGH: 0579 return s_ac3General 0580 + "<br>" + i18n("With this setting K3b will use the Dolby Digital audio stream " 0581 "from the source DVD without changing it.") 0582 + "<br>" + i18n("Use this setting to preserve 5.1 channel sound from the DVD."); 0583 case AUDIO_CODEC_MP3: 0584 return i18n("MPEG1 Layer III is better known as MP3 and is the most used lossy audio format.") 0585 + "<br>" + i18n("With this setting K3b will create a two-channel stereo MPEG1 Layer III audio stream."); 0586 default: 0587 return "unknown audio codec"; 0588 } 0589 } 0590 0591 0592 bool K3b::VideoDVDTitleTranscodingJob::transcodeBinaryHasSupportFor( K3b::VideoDVDTitleTranscodingJob::VideoCodec codec, const K3b::ExternalBin* bin ) 0593 { 0594 static const char* const s_codecFeatures[] = { "xvid", "ffmpeg" }; 0595 if( !bin ) 0596 bin = k3bcore->externalBinManager()->binObject("transcode"); 0597 if( !bin ) 0598 return false; 0599 return bin->hasFeature( QString::fromLatin1( s_codecFeatures[(int)codec] ) ); 0600 } 0601 0602 0603 bool K3b::VideoDVDTitleTranscodingJob::transcodeBinaryHasSupportFor( K3b::VideoDVDTitleTranscodingJob::AudioCodec codec, const K3b::ExternalBin* bin ) 0604 { 0605 static const char* const s_codecFeatures[] = { "lame", "ac3", "ac3" }; 0606 if( !bin ) 0607 bin = k3bcore->externalBinManager()->binObject("transcode"); 0608 if( !bin ) 0609 return false; 0610 return bin->hasFeature( QString::fromLatin1( s_codecFeatures[(int)codec] ) ); 0611 } 0612 0613 #include "moc_k3bvideodvdtitletranscodingjob.cpp"