File indexing completed on 2024-05-05 04:50:58

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"