diff options
Diffstat (limited to 'libk3b/jobs/k3bvideodvdtitletranscodingjob.cpp')
-rw-r--r-- | libk3b/jobs/k3bvideodvdtitletranscodingjob.cpp | 583 |
1 files changed, 583 insertions, 0 deletions
diff --git a/libk3b/jobs/k3bvideodvdtitletranscodingjob.cpp b/libk3b/jobs/k3bvideodvdtitletranscodingjob.cpp new file mode 100644 index 0000000..9fec637 --- /dev/null +++ b/libk3b/jobs/k3bvideodvdtitletranscodingjob.cpp @@ -0,0 +1,583 @@ +/* + * + * $Id: sourceheader 511311 2006-02-19 14:51:05Z trueg $ + * Copyright (C) 2006 Sebastian Trueg <[email protected]> + * + * This file is part of the K3b project. + * Copyright (C) 1998-2007 Sebastian Trueg <[email protected]> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * See the file "COPYING" for the exact licensing terms. + */ + +#include "k3bvideodvdtitletranscodingjob.h" + +#include <k3bexternalbinmanager.h> +#include <k3bprocess.h> +#include <k3bcore.h> +#include <k3bglobals.h> + +#include <klocale.h> +#include <kdebug.h> +#include <kstandarddirs.h> + +#include <qfile.h> +#include <qfileinfo.h> + + +class K3bVideoDVDTitleTranscodingJob::Private +{ +public: + const K3bExternalBin* usedTranscodeBin; + + K3bProcess* process; + + QString twoPassEncodingLogFile; + + int currentEncodingPass; + + bool canceled; + + int lastProgress; + int lastSubProgress; +}; + + + +K3bVideoDVDTitleTranscodingJob::K3bVideoDVDTitleTranscodingJob( K3bJobHandler* hdl, QObject* parent ) + : K3bJob( hdl, parent ), + m_clippingTop( 0 ), + m_clippingBottom( 0 ), + m_clippingLeft( 0 ), + m_clippingRight( 0 ), + m_width( 0 ), + m_height( 0 ), + m_titleNumber( 1 ), + m_audioStreamIndex( 0 ), + m_videoCodec( VIDEO_CODEC_FFMPEG_MPEG4 ), + m_audioCodec( AUDIO_CODEC_MP3 ), + m_videoBitrate( 1800 ), + m_audioBitrate( 128 ), + m_audioVBR( false ), + m_resampleAudio( false ), + m_twoPassEncoding( false ), + m_lowPriority( true ) +{ + d = new Private; + d->process = 0; +} + + +K3bVideoDVDTitleTranscodingJob::~K3bVideoDVDTitleTranscodingJob() +{ + delete d->process; + delete d; +} + + +void K3bVideoDVDTitleTranscodingJob::start() +{ + jobStarted(); + + d->canceled = false; + d->lastProgress = 0; + + d->usedTranscodeBin = k3bcore->externalBinManager()->binObject("transcode"); + if( !d->usedTranscodeBin ) { + emit infoMessage( i18n("%1 executable could not be found.").arg("transcode"), ERROR ); + jobFinished( false ); + return; + } + + if( d->usedTranscodeBin->version < K3bVersion( 1, 0, 0 ) ){ + emit infoMessage( i18n("%1 version %2 is too old.") + .arg("transcode") + .arg(d->usedTranscodeBin->version), ERROR ); + jobFinished( false ); + return; + } + + emit debuggingOutput( "Used versions", "transcode: " + d->usedTranscodeBin->version ); + + if( !d->usedTranscodeBin->copyright.isEmpty() ) + emit infoMessage( i18n("Using %1 %2 - Copyright (C) %3") + .arg(d->usedTranscodeBin->name()) + .arg(d->usedTranscodeBin->version) + .arg(d->usedTranscodeBin->copyright), INFO ); + + // + // Let's take a look at the filename + // + if( m_filename.isEmpty() ) { + m_filename = K3b::findTempFile( "avi" ); + } + else { + // let's see if the directory exists and we can write to it + QFileInfo fileInfo( m_filename ); + QFileInfo dirInfo( fileInfo.dirPath() ); + if( !dirInfo.exists() && !KStandardDirs::makeDir( dirInfo.absFilePath() ) ) { + emit infoMessage( i18n("Unable to create folder '%1'").arg(dirInfo.filePath()), ERROR ); + return; + } + else if( !dirInfo.isDir() || !dirInfo.isWritable() ) { + emit infoMessage( i18n("Invalid filename: '%1'").arg(m_filename), ERROR ); + jobFinished( false ); + return; + } + } + + // + // Determine a log file for two-pass encoding + // + d->twoPassEncodingLogFile = K3b::findTempFile( "log" ); + + emit newTask( i18n("Transcoding title %1 from Video DVD %2").arg(m_titleNumber).arg(m_dvd.volumeIdentifier()) ); + + // + // Ok then, let's begin + // + startTranscode( m_twoPassEncoding ? 1 : 0 ); +} + + +void K3bVideoDVDTitleTranscodingJob::startTranscode( int pass ) +{ + d->currentEncodingPass = pass; + d->lastSubProgress = 0; + + QString videoCodecString; + switch( m_videoCodec ) { + case VIDEO_CODEC_XVID: + videoCodecString = "xvid"; + break; + + case VIDEO_CODEC_FFMPEG_MPEG4: + videoCodecString = "ffmpeg"; + break; + + default: + emit infoMessage( i18n("Invalid Video codec set: %1").arg(m_videoCodec), ERROR ); + jobFinished( false ); + return; + } + + QString audioCodecString; + switch( m_audioCodec ) { + case AUDIO_CODEC_MP3: + audioCodecString = "0x55"; + break; + + // ogg only works (as in: transcode does something) with .y <codec>,ogg + // but then the video is garbage (at least to xine and mplayer on my system) + // case AUDIO_CODEC_OGG_VORBIS: + // audioCodecString = "0xfffe"; + // break; + + case AUDIO_CODEC_AC3_STEREO: + case AUDIO_CODEC_AC3_PASSTHROUGH: + audioCodecString = "0x2000"; + break; + + default: + emit infoMessage( i18n("Invalid Audio codec set: %1").arg(m_audioCodec), ERROR ); + jobFinished( false ); + return; + } + + // + // prepare the process + // + delete d->process; + d->process = new K3bProcess(); + d->process->setSuppressEmptyLines(true); + d->process->setSplitStdout(true); + connect( d->process, SIGNAL(stderrLine(const QString&)), this, SLOT(slotTranscodeStderr(const QString&)) ); + connect( d->process, SIGNAL(stdoutLine(const QString&)), this, SLOT(slotTranscodeStderr(const QString&)) ); + connect( d->process, SIGNAL(processExited(KProcess*)), this, SLOT(slotTranscodeExited(KProcess*)) ); + + // the executable + *d->process << d->usedTranscodeBin; + + // low priority + if( m_lowPriority ) + *d->process << "--nice" << "19"; + + // we only need 100 steps, but to make sure we use 150 + if ( d->usedTranscodeBin->version.simplify() >= K3bVersion( 1, 1, 0 ) ) + *d->process << "--progress_meter" << "2" << "--progress_rate" << QString::number(m_dvd[m_titleNumber-1].playbackTime().totalFrames()/150); + else + *d->process << "--print_status" << QString::number(m_dvd[m_titleNumber-1].playbackTime().totalFrames()/150); + + // the input + *d->process << "-i" << m_dvd.device()->blockDeviceName(); + + // just to make sure + *d->process << "-x" << "dvd"; + + // select the title number + *d->process << "-T" << QString("%1,-1,1").arg( m_titleNumber ); + + // select the audio stream to extract + if ( m_dvd[m_titleNumber-1].numAudioStreams() > 0 ) + *d->process << "-a" << QString::number( m_audioStreamIndex ); + + // clipping + *d->process << "-j" << QString("%1,%2,%3,%4") + .arg(m_clippingTop) + .arg(m_clippingLeft) + .arg(m_clippingBottom) + .arg(m_clippingRight); + + // select the encoding type (single pass or two-pass) and the log file for two-pass encoding + // the latter is unused for pass = 0 + *d->process << "-R" << QString("%1,%2").arg( pass ).arg( d->twoPassEncodingLogFile ); + + // depending on the pass we use different options + if( pass != 1 ) { + // select video codec + *d->process << "-y" << videoCodecString; + + // select the audio codec to use + *d->process << "-N" << audioCodecString; + + if( m_audioCodec == AUDIO_CODEC_AC3_PASSTHROUGH ) { + // keep 5.1 sound + *d->process << "-A"; + } + else { + // audio quality settings + *d->process << "-b" << QString("%1,%2").arg(m_audioBitrate).arg(m_audioVBR ? 1 : 0); + + // resample audio stream to 44.1 khz + if( m_resampleAudio ) + *d->process << "-E" << "44100"; + } + + // the output filename + *d->process << "-o" << m_filename; + } + else { + // gather information about the video stream, ignore audio + *d->process << "-y" << QString("%1,null").arg( videoCodecString ); + + // we ignore the output from the first pass + *d->process << "-o" << "/dev/null"; + } + + // choose the ffmpeg codec + if( m_videoCodec == VIDEO_CODEC_FFMPEG_MPEG4 ) { + *d->process << "-F" << "mpeg4"; + } + + // video bitrate + *d->process << "-w" << QString::number( m_videoBitrate ); + + // video resizing + int usedWidth = m_width; + int usedHeight = m_height; + if( m_width == 0 || m_height == 0 ) { + // + // The "real" size of the video, considering anamorph encoding + // + int realHeight = m_dvd[m_titleNumber-1].videoStream().realPictureHeight(); + int readWidth = m_dvd[m_titleNumber-1].videoStream().realPictureWidth(); + + // + // The clipped size with the correct aspect ratio + // + int clippedHeight = realHeight - m_clippingTop - m_clippingBottom; + int clippedWidth = readWidth - m_clippingLeft - m_clippingRight; + + // + // Now simply resize the clipped video to the wanted size + // + if( usedWidth > 0 ) { + usedHeight = clippedHeight * usedWidth / clippedWidth; + } + else { + if( usedHeight == 0 ) { + // + // This is the default case in which both m_width and m_height are 0. + // The result will be a size of clippedWidth x clippedHeight + // + usedHeight = clippedHeight; + } + usedWidth = clippedWidth * usedHeight / clippedHeight; + } + } + + // + // Now make sure both width and height are multiple of 16 the simple way + // + usedWidth -= usedWidth%16; + usedHeight -= usedHeight%16; + + // we only give information about the resizing of the video once + if( pass < 2 ) + emit infoMessage( i18n("Resizing picture of title %1 to %2x%3").arg(m_titleNumber).arg(usedWidth).arg(usedHeight), INFO ); + *d->process << "-Z" << QString("%1x%2").arg(usedWidth).arg(usedHeight); + + // additional user parameters from config + const QStringList& params = d->usedTranscodeBin->userParameters(); + for( QStringList::const_iterator it = params.begin(); it != params.end(); ++it ) + *d->process << *it; + + // produce some debugging output + kdDebug() << "***** transcode parameters:\n"; + const QValueList<QCString>& args = d->process->args(); + QString s; + for( QValueList<QCString>::const_iterator it = args.begin(); it != args.end(); ++it ) { + s += *it + " "; + } + kdDebug() << s << flush << endl; + emit debuggingOutput( d->usedTranscodeBin->name() + " command:", s); + + // start the process + if( !d->process->start( KProcess::NotifyOnExit, KProcess::All ) ) { + // something went wrong when starting the program + // it "should" be the executable + emit infoMessage( i18n("Could not start %1.").arg(d->usedTranscodeBin->name()), K3bJob::ERROR ); + jobFinished(false); + } + else { + if( pass == 0 ) + emit newSubTask( i18n("Single-pass Encoding") ); + else if( pass == 1 ) + emit newSubTask( i18n("Two-pass Encoding: First Pass") ); + else + emit newSubTask( i18n("Two-pass Encoding: Second Pass") ); + + emit subPercent( 0 ); + } +} + + +void K3bVideoDVDTitleTranscodingJob::cancel() +{ + // FIXME: do not cancel before one frame has been encoded. transcode seems to hang then + // find a way to determine all subprocess ids to kill all of them + d->canceled = true; + if( d->process && d->process->isRunning() ) + d->process->kill(); +} + + +void K3bVideoDVDTitleTranscodingJob::cleanup( bool success ) +{ + if( QFile::exists( d->twoPassEncodingLogFile ) ) { + QFile::remove( d->twoPassEncodingLogFile ); + } + + if( !success && QFile::exists( m_filename ) ) { + emit infoMessage( i18n("Removing incomplete video file '%1'").arg(m_filename), INFO ); + QFile::remove( m_filename ); + } +} + + +void K3bVideoDVDTitleTranscodingJob::slotTranscodeStderr( const QString& line ) +{ + emit debuggingOutput( "transcode", line ); + + // parse progress + // encoding frames [000000-000144], 27.58 fps, EMT: 0:00:05, ( 0| 0| 0) + if( line.startsWith( "encoding frame" ) ) { + int pos1 = line.find( '-', 15 ); + int pos2 = line.find( ']', pos1+1 ); + if( pos1 > 0 && pos2 > 0 ) { + bool ok; + int encodedFrames = line.mid( pos1+1, pos2-pos1-1 ).toInt( &ok ); + if( ok ) { + int progress = 100 * encodedFrames / m_dvd[m_titleNumber-1].playbackTime().totalFrames(); + + if( progress > d->lastSubProgress ) { + d->lastSubProgress = progress; + emit subPercent( progress ); + } + + if( m_twoPassEncoding ) { + progress /= 2; + if( d->currentEncodingPass == 2 ) + progress += 50; + } + + if( progress > d->lastProgress ) { + d->lastProgress = progress; + emit percent( progress ); + } + } + } + } +} + + +void K3bVideoDVDTitleTranscodingJob::slotTranscodeExited( KProcess* p ) +{ + if( d->canceled ) { + emit canceled(); + cleanup( false ); + jobFinished( false ); + } + else if( p->normalExit() ) { + switch( p->exitStatus() ) { + case 0: + if( d->currentEncodingPass == 1 ) { + emit percent( 50 ); + // start second encoding pass + startTranscode( 2 ); + } + else { + emit percent( 100 ); + cleanup( true ); + jobFinished( true ); + } + break; + + default: + // FIXME: error handling + + emit infoMessage( i18n("%1 returned an unknown error (code %2).") + .arg(d->usedTranscodeBin->name()).arg(p->exitStatus()), + K3bJob::ERROR ); + emit infoMessage( i18n("Please send me an email with the last output."), K3bJob::ERROR ); + + cleanup( false ); + jobFinished( false ); + } + } + else { + cleanup( false ); + emit infoMessage( i18n("Execution of %1 failed.").arg("transcode"), ERROR ); + emit infoMessage( i18n("Please consult the debugging output for details."), ERROR ); + jobFinished( false ); + } +} + + +void K3bVideoDVDTitleTranscodingJob::setClipping( int top, int left, int bottom, int right ) +{ + m_clippingTop = top; + m_clippingLeft = left; + m_clippingBottom = bottom; + m_clippingRight = right; + + // + // transcode seems unable to handle different clipping values for left and right + // + m_clippingLeft = m_clippingRight = QMIN( m_clippingRight, m_clippingLeft ); +} + + +void K3bVideoDVDTitleTranscodingJob::setSize( int width, int height ) +{ + m_width = width; + m_height = height; +} + + +QString K3bVideoDVDTitleTranscodingJob::audioCodecString( K3bVideoDVDTitleTranscodingJob::AudioCodec codec ) +{ + switch( codec ) { + case AUDIO_CODEC_AC3_STEREO: + return i18n("AC3 (Stereo)"); + case AUDIO_CODEC_AC3_PASSTHROUGH: + return i18n("AC3 (Pass-through)"); + case AUDIO_CODEC_MP3: + return i18n("MPEG1 Layer III"); + default: + return "unknown audio codec"; + } +} + + +QString K3bVideoDVDTitleTranscodingJob::videoCodecString( K3bVideoDVDTitleTranscodingJob::VideoCodec codec ) +{ + switch( codec ) { + case VIDEO_CODEC_FFMPEG_MPEG4: + return i18n("MPEG4 (FFMPEG)"); + case VIDEO_CODEC_XVID: + return i18n("XviD"); + default: + return "unknown video codec"; + } +} + + +QString K3bVideoDVDTitleTranscodingJob::videoCodecDescription( K3bVideoDVDTitleTranscodingJob::VideoCodec codec ) +{ + switch( codec ) { + case VIDEO_CODEC_FFMPEG_MPEG4: + return i18n("FFmpeg is an open-source project trying to support most video and audio codecs used " + "these days. Its subproject libavcodec forms the basis for multimedia players such as " + "xine or mplayer.") + + "<br>" + + i18n("FFmpeg contains an implementation of the MPEG-4 video encoding standard which produces " + "high quality results."); + case VIDEO_CODEC_XVID: + return i18n("XviD is a free and open source MPEG-4 video codec. XviD was created by a group of " + "volunteer programmers after the OpenDivX source was closed in July 2001.") + + "<br>" + + i18n("XviD features MPEG-4 Advanced Profile settings such as b-frames, global " + "and quarter pixel motion compensation, lumi masking, trellis quantization, and " + "H.263, MPEG and custom quantization matrices.") + + "<br>" + + i18n("XviD is a primary competitor of DivX (XviD being DivX spelled backwards). " + "While DivX is closed source and may only run on Windows, Mac OS and Linux, " + "XviD is open source and can potentially run on any platform.") + + "<br><em>" + + i18n("(Description taken from the Wikipedia article)") + + "</em>"; + default: + return "unknown video codec"; + } +} + + +QString K3bVideoDVDTitleTranscodingJob::audioCodecDescription( K3bVideoDVDTitleTranscodingJob::AudioCodec codec ) +{ + static QString s_ac3General = i18n("AC3, better known as Dolby Digital is standardized as ATSC A/52. " + "It contains up to 6 total channels of sound."); + switch( codec ) { + case AUDIO_CODEC_AC3_STEREO: + return s_ac3General + + "<br>" + i18n("With this setting K3b will create a two-channel stereo " + "Dolby Digital audio stream."); + case AUDIO_CODEC_AC3_PASSTHROUGH: + return s_ac3General + + "<br>" + i18n("With this setting K3b will use the Dolby Digital audio stream " + "from the source DVD without changing it.") + + "<br>" + i18n("Use this setting to preserve 5.1 channel sound from the DVD."); + case AUDIO_CODEC_MP3: + return i18n("MPEG1 Layer III is better known as MP3 and is the most used lossy audio format.") + + "<br>" + i18n("With this setting K3b will create a two-channel stereo MPEG1 Layer III audio stream."); + default: + return "unknown audio codec"; + } +} + + +bool K3bVideoDVDTitleTranscodingJob::transcodeBinaryHasSupportFor( K3bVideoDVDTitleTranscodingJob::VideoCodec codec, const K3bExternalBin* bin ) +{ + static char* s_codecFeatures[] = { "xvid", "ffmpeg" }; + if( !bin ) + bin = k3bcore->externalBinManager()->binObject("transcode"); + if( !bin ) + return false; + return bin->hasFeature( QString::fromLatin1( s_codecFeatures[(int)codec] ) ); +} + + +bool K3bVideoDVDTitleTranscodingJob::transcodeBinaryHasSupportFor( K3bVideoDVDTitleTranscodingJob::AudioCodec codec, const K3bExternalBin* bin ) +{ + static char* s_codecFeatures[] = { "lame", "ac3", "ac3" }; + if( !bin ) + bin = k3bcore->externalBinManager()->binObject("transcode"); + if( !bin ) + return false; + return bin->hasFeature( QString::fromLatin1( s_codecFeatures[(int)codec] ) ); +} + +#include "k3bvideodvdtitletranscodingjob.moc" |