File indexing completed on 2024-05-05 04:49:24

0001 /****************************************************************************************
0002  * Copyright (c) 2010 Téo Mrnjavac <teo@kde.org>                                        *
0003  *                                                                                      *
0004  * This program is free software; you can redistribute it and/or modify it under        *
0005  * the terms of the GNU General Public License as published by the Free Software        *
0006  * Foundation; either version 2 of the License, or (at your option) any later           *
0007  * version.                                                                             *
0008  *                                                                                      *
0009  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
0010  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
0011  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
0012  *                                                                                      *
0013  * You should have received a copy of the GNU General Public License along with         *
0014  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
0015  ****************************************************************************************/
0016 
0017 #include "TranscodingJob.h"
0018 
0019 #include "core/support/Debug.h"
0020 #include "core/transcoding/TranscodingController.h"
0021 
0022 #include <KProcess>
0023 
0024 #include <QFile>
0025 #include <QTimer>
0026 
0027 namespace Transcoding
0028 {
0029 
0030 Job::Job( const QUrl &src,
0031                             const QUrl &dest,
0032                             const Transcoding::Configuration &configuration,
0033                             QObject *parent )
0034     : KJob( parent )
0035     , m_src( src )
0036     , m_dest( dest )
0037     , m_configuration( configuration )
0038     , m_duration( -1 )
0039 {
0040     init();
0041 }
0042 
0043 Job::Job( QUrl &src,
0044                             const Transcoding::Configuration &configuration,
0045                             QObject *parent )
0046     : KJob( parent )
0047     , m_src( src )
0048     , m_dest( src )
0049     , m_configuration( configuration )
0050     , m_duration( -1 )
0051 {
0052     QString fileExtension = Amarok::Components::transcodingController()->format( configuration.encoder() )->fileExtension();
0053     if( !( fileExtension.isEmpty() ) )
0054     {
0055         QString destPath = src.path();
0056         destPath.truncate( destPath.lastIndexOf( QLatin1Char('.') ) + 1 );
0057         destPath.append( fileExtension );
0058         m_dest.setPath( destPath );
0059     }
0060     init();
0061 }
0062 
0063 void
0064 Job::init()
0065 {
0066     m_transcoder = new KProcess( this );
0067 
0068     m_transcoder->setOutputChannelMode( KProcess::MergedChannels );
0069 
0070     //First the executable...
0071     m_transcoder->setProgram( QStringLiteral("ffmpeg") );
0072     //... prevent ffmpeg from being interactive when destination file already exists. We
0073     //    would use -n to exit immediately, but libav's ffmpeg doesn't support it, so we
0074     //    check for destination file existence manually and pass -y (overwrite) to avoid
0075     //    race condition
0076     *m_transcoder << QStringLiteral( "-y" );
0077     //... then we'd have the infile configuration followed by "-i" and the infile path...
0078     *m_transcoder << QStringLiteral( "-i" )
0079                   << m_src.path();
0080     //... and finally, outfile configuration followed by the outfile path.
0081     const Transcoding::Format *format = Amarok::Components::transcodingController()->format( m_configuration.encoder() );
0082     *m_transcoder << format->ffmpegParameters( m_configuration )
0083                   << m_dest.path();
0084 
0085     connect( m_transcoder, &KProcess::readyRead,
0086              this, &Job::processOutput );
0087     connect( m_transcoder, QOverload<int, QProcess::ExitStatus>::of(&KProcess::finished),
0088              this, &Job::transcoderDone );
0089 }
0090 
0091 void
0092 Job::start()
0093 {
0094     DEBUG_BLOCK
0095     if( QFile::exists( m_dest.path() ) )
0096     {
0097         debug() << "Not starting ffmpeg encoder, file already exists:" << m_dest.path();
0098         QTimer::singleShot( 0, this, &Job::transcoderDoneDefault );
0099     }
0100     else
0101     {
0102         QString commandline = QStringLiteral( "'" ) + m_transcoder->program().join(QStringLiteral("' '")) + QStringLiteral( "'" );
0103         debug()<< "Calling" << commandline.toLocal8Bit().constData();
0104         m_transcoder->start();
0105     }
0106 }
0107 
0108 void
0109 Job::transcoderDone( int exitCode, QProcess::ExitStatus exitStatus ) //SLOT
0110 {
0111     if( exitCode == 0 && exitStatus == QProcess::NormalExit )
0112         debug() << "YAY, transcoding done!";
0113     else
0114     {
0115         debug() << "NAY, transcoding fail!";
0116         setError( KJob::UserDefinedError );
0117         setErrorText( QStringLiteral( "Calling `" ) + m_transcoder->program().join(QStringLiteral(" ")) + "` failed" );
0118     }
0119     emitResult();
0120 }
0121 
0122 void
0123 Job::transcoderDoneDefault()
0124 {
0125     transcoderDone( -1, QProcess::CrashExit );
0126 }
0127 
0128 void
0129 Job::processOutput()
0130 {
0131     QString output = QString::fromLocal8Bit( m_transcoder->readAllStandardOutput().data() );
0132     if( output.simplified().isEmpty() )
0133         return;
0134     foreach( const QString &line, output.split( QChar( '\n' ) ) )
0135         debug() << "ffmpeg:" << line.toLocal8Bit().constData();
0136 
0137     if( m_duration == -1 )
0138     {
0139         m_duration = computeDuration( output );
0140         if( m_duration >= 0 )
0141             setTotalAmount( KJob::Bytes, m_duration ); //Nothing better than bytes I can think of
0142     }
0143 
0144     qint64 progress = computeProgress( output  );
0145     if( progress > -1 )
0146         setProcessedAmount( KJob::Bytes, progress );
0147 }
0148 
0149 inline qint64
0150 Job::computeDuration( const QString &output )
0151 {
0152     //We match something like "Duration: 00:04:33.60"
0153     QRegExp matchDuration( "Duration: (\\d{2,}):(\\d{2}):(\\d{2})\\.(\\d{2})" );
0154 
0155     if( output.contains( matchDuration ) )
0156     {
0157         //duration is in csec
0158         qint64 duration = matchDuration.cap( 1 ).toLong() * 60 * 60 * 100 +
0159                           matchDuration.cap( 2 ).toInt()  * 60 * 100 +
0160                           matchDuration.cap( 3 ).toInt()  * 100 +
0161                           matchDuration.cap( 4 ).toInt();
0162         return duration;
0163     }
0164     else
0165         return -1;
0166 }
0167 
0168 inline qint64
0169 Job::computeProgress( const QString &output )
0170 {
0171     //Output is like size=     323kB time=18.10 bitrate= 146.0kbits/s
0172     //We're going to use the "time" column, which counts the elapsed time in seconds.
0173     QRegExp matchTime( "time=(\\d+)\\.(\\d{2})" );
0174 
0175     if( output.contains( matchTime ) )
0176     {
0177         qint64 time = matchTime.cap( 1 ).toLong() * 100 +
0178                       matchTime.cap( 2 ).toInt();
0179         return time;
0180     }
0181     else
0182         return -1;
0183 }
0184 
0185 } //namespace Transcoding