2018-02-27 18:06:05 +01:00
/*
* Strawberry Music Player
* This file was part of Clementine .
* Copyright 2010 , David Sansome < me @ davidsansome . com >
2021-03-20 21:14:47 +01:00
* Copyright 2018 - 2021 , Jonas Kvinge < jonas @ jkvinge . net >
2018-02-27 18:06:05 +01:00
*
* Strawberry 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 3 of the License , or
* ( at your option ) any later version .
*
* Strawberry is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
*
* You should have received a copy of the GNU General Public License
* along with Strawberry . If not , see < http : //www.gnu.org/licenses/>.
2018-08-09 18:10:03 +02:00
*
2018-02-27 18:06:05 +01:00
*/
# include "config.h"
2023-07-21 05:25:57 +02:00
# include <QtGlobal>
2018-10-19 20:18:46 +02:00
# include <algorithm>
2023-07-21 05:25:57 +02:00
# include <memory>
2023-01-12 23:37:12 +01:00
# include <glib.h>
# include <glib/gtypes.h>
2018-05-01 00:41:33 +02:00
# include <gst/gst.h>
2018-02-27 18:06:05 +01:00
2018-05-01 00:41:33 +02:00
# include <QThread>
2018-02-27 18:06:05 +01:00
# include <QCoreApplication>
2019-01-26 14:57:25 +01:00
# include <QStandardPaths>
2018-05-01 00:41:33 +02:00
# include <QByteArray>
2018-02-27 18:06:05 +01:00
# include <QDir>
2019-01-26 14:57:25 +01:00
# include <QFileInfo>
2018-05-01 00:41:33 +02:00
# include <QList>
# include <QMap>
# include <QVariant>
# include <QString>
2018-02-27 18:06:05 +01:00
# include <QSettings>
# include "core/logging.h"
2023-07-21 05:55:24 +02:00
# include "core/shared_ptr.h"
2018-02-27 18:06:05 +01:00
# include "core/signalchecker.h"
2024-04-11 02:56:01 +02:00
# include "core/settings.h"
2018-05-01 00:41:33 +02:00
# include "transcoder.h"
2018-02-27 18:06:05 +01:00
2024-09-07 04:24:14 +02:00
using namespace Qt : : StringLiterals ;
2023-07-21 05:55:24 +02:00
using std : : make_shared ;
2018-02-27 18:06:05 +01:00
int Transcoder : : JobFinishedEvent : : sEventType = - 1 ;
2021-06-20 19:04:08 +02:00
TranscoderPreset : : TranscoderPreset ( const Song : : FileType filetype , const QString & name , const QString & extension , const QString & codec_mimetype , const QString & muxer_mimetype )
: filetype_ ( filetype ) ,
2018-02-27 18:06:05 +01:00
name_ ( name ) ,
extension_ ( extension ) ,
codec_mimetype_ ( codec_mimetype ) ,
muxer_mimetype_ ( muxer_mimetype ) { }
GstElement * Transcoder : : CreateElement ( const QString & factory_name , GstElement * bin , const QString & name ) {
GstElement * ret = gst_element_factory_make ( factory_name . toLatin1 ( ) . constData ( ) , name . isNull ( ) ? factory_name . toLatin1 ( ) . constData ( ) : name . toLatin1 ( ) . constData ( ) ) ;
if ( ret & & bin ) gst_bin_add ( GST_BIN ( bin ) , ret ) ;
2023-01-12 23:37:12 +01:00
if ( ret ) {
SetElementProperties ( factory_name , G_OBJECT ( ret ) ) ;
2018-02-27 18:06:05 +01:00
}
else {
2024-08-25 01:06:30 +02:00
Q_EMIT LogLine ( tr ( " Could not create the GStreamer element \" %1 \" - make sure you have all the required GStreamer plugins installed " ) . arg ( factory_name ) ) ;
2018-02-27 18:06:05 +01:00
}
return ret ;
}
struct SuitableElement {
2020-06-26 22:41:38 +02:00
explicit SuitableElement ( const QString & name = QString ( ) , int rank = 0 ) : name_ ( name ) , rank_ ( rank ) { }
2018-02-27 18:06:05 +01:00
bool operator < ( const SuitableElement & other ) const {
return rank_ < other . rank_ ;
}
QString name_ ;
int rank_ ;
} ;
2024-07-11 09:31:51 +02:00
GstElement * Transcoder : : CreateElementForMimeType ( GstElementFactoryListType element_type , const QString & mime_type , GstElement * bin ) {
2018-02-27 18:06:05 +01:00
if ( mime_type . isEmpty ( ) ) return nullptr ;
2019-01-24 19:16:39 +01:00
// HACK: Force mp4mux because it doesn't set any useful src caps
2024-09-07 04:24:14 +02:00
if ( mime_type = = " audio/mp4 " _L1 ) {
Q_EMIT LogLine ( QStringLiteral ( " Using '%1' (rank %2) " ).arg( " mp4mux " _L1).arg(-1)) ;
2024-04-09 23:20:26 +02:00
return CreateElement ( QStringLiteral ( " mp4mux " ) , bin ) ;
2018-02-27 18:06:05 +01:00
}
2018-05-01 00:41:33 +02:00
// Keep track of all the suitable elements we find and figure out which is the best at the end.
2018-02-27 18:06:05 +01:00
QList < SuitableElement > suitable_elements_ ;
// The caps we're trying to find
GstCaps * target_caps = gst_caps_from_string ( mime_type . toUtf8 ( ) . constData ( ) ) ;
GstRegistry * registry = gst_registry_get ( ) ;
GList * const features = gst_registry_get_feature_list ( registry , GST_TYPE_ELEMENT_FACTORY ) ;
2021-08-23 21:21:08 +02:00
for ( GList * f = features ; f ; f = g_list_next ( f ) ) {
2020-04-23 21:08:28 +02:00
GstElementFactory * factory = GST_ELEMENT_FACTORY ( f - > data ) ;
2018-02-27 18:06:05 +01:00
// Is this the right type of plugin?
2024-07-11 09:31:51 +02:00
if ( gst_element_factory_list_is_type ( factory , element_type ) ) {
// check if the element factory supports the target caps
if ( gst_element_factory_can_src_any_caps ( factory , target_caps ) ) {
const QString name = QString : : fromUtf8 ( GST_OBJECT_NAME ( factory ) ) ;
int rank = static_cast < int > ( gst_plugin_feature_get_rank ( GST_PLUGIN_FEATURE ( factory ) ) ) ;
2024-09-07 04:24:14 +02:00
if ( name . startsWith ( " avmux " _L1 ) | | name . startsWith ( " avenc " _L1 ) ) {
2024-07-11 09:31:51 +02:00
rank = - 1 ; // ffmpeg usually sucks
2018-02-27 18:06:05 +01:00
}
2024-07-11 09:31:51 +02:00
suitable_elements_ < < SuitableElement ( name , rank ) ;
2018-02-27 18:06:05 +01:00
}
}
}
gst_plugin_feature_list_free ( features ) ;
gst_caps_unref ( target_caps ) ;
if ( suitable_elements_ . isEmpty ( ) ) return nullptr ;
// Sort by rank
2018-10-19 20:18:46 +02:00
std : : sort ( suitable_elements_ . begin ( ) , suitable_elements_ . end ( ) ) ;
2018-02-27 18:06:05 +01:00
const SuitableElement & best = suitable_elements_ . last ( ) ;
2024-08-25 01:06:30 +02:00
Q_EMIT LogLine ( QStringLiteral ( " Using '%1' (rank %2) " ).arg(best.name_).arg(best.rank_)) ;
2018-02-27 18:06:05 +01:00
2024-09-07 04:24:14 +02:00
if ( best . name_ = = " lamemp3enc " _L1 ) {
2018-05-01 00:41:33 +02:00
// Special case: we need to add xingmux and id3v2mux to the pipeline when using lamemp3enc because it doesn't write the VBR or ID3v2 headers itself.
2018-02-27 18:06:05 +01:00
2024-08-25 01:06:30 +02:00
Q_EMIT LogLine ( QStringLiteral ( " Adding xingmux and id3v2mux to the pipeline " ) ) ;
2018-02-27 18:06:05 +01:00
// Create the bin
GstElement * mp3bin = gst_bin_new ( " mp3bin " ) ;
gst_bin_add ( GST_BIN ( bin ) , mp3bin ) ;
// Create the elements
2024-04-09 23:20:26 +02:00
GstElement * lame = CreateElement ( QStringLiteral ( " lamemp3enc " ) , mp3bin ) ;
GstElement * xing = CreateElement ( QStringLiteral ( " xingmux " ) , mp3bin ) ;
GstElement * id3v2 = CreateElement ( QStringLiteral ( " id3v2mux " ) , mp3bin ) ;
2018-02-27 18:06:05 +01:00
if ( ! lame | | ! xing | | ! id3v2 ) {
return nullptr ;
}
// Link the elements together
gst_element_link_many ( lame , xing , id3v2 , nullptr ) ;
// Link the bin's ghost pads to the elements on each end
GstPad * pad = gst_element_get_static_pad ( lame , " sink " ) ;
gst_element_add_pad ( mp3bin , gst_ghost_pad_new ( " sink " , pad ) ) ;
gst_object_unref ( GST_OBJECT ( pad ) ) ;
pad = gst_element_get_static_pad ( id3v2 , " src " ) ;
gst_element_add_pad ( mp3bin , gst_ghost_pad_new ( " src " , pad ) ) ;
gst_object_unref ( GST_OBJECT ( pad ) ) ;
return mp3bin ;
}
else {
return CreateElement ( best . name_ , bin ) ;
}
2022-07-16 16:31:04 +02:00
2018-02-27 18:06:05 +01:00
}
Transcoder : : JobFinishedEvent : : JobFinishedEvent ( JobState * state , bool success )
2022-06-13 00:23:42 +02:00
: QEvent ( static_cast < QEvent : : Type > ( sEventType ) ) , state_ ( state ) , success_ ( success ) { }
2018-02-27 18:06:05 +01:00
2021-06-20 19:04:08 +02:00
void Transcoder : : JobState : : PostFinished ( const bool success ) {
2018-02-27 18:06:05 +01:00
if ( success ) {
2024-08-25 01:06:30 +02:00
Q_EMIT parent_ - > LogLine ( tr ( " Successfully written %1 " ) . arg ( QDir : : toNativeSeparators ( job_ . output ) ) ) ;
2018-02-27 18:06:05 +01:00
}
QCoreApplication : : postEvent ( parent_ , new Transcoder : : JobFinishedEvent ( this , success ) ) ;
}
Transcoder : : Transcoder ( QObject * parent , const QString & settings_postfix )
2019-01-06 14:34:50 +01:00
: QObject ( parent ) ,
2021-07-11 09:49:38 +02:00
max_threads_ ( QThread : : idealThreadCount ( ) ) ,
settings_postfix_ ( settings_postfix ) {
2018-02-27 18:06:05 +01:00
if ( JobFinishedEvent : : sEventType = = - 1 )
JobFinishedEvent : : sEventType = QEvent : : registerEventType ( ) ;
2020-10-17 17:29:09 +02:00
// Initialize some settings for the lamemp3enc element.
2024-04-11 02:56:01 +02:00
Settings s ;
2024-09-07 04:24:14 +02:00
s . beginGroup ( " Transcoder/lamemp3enc " _L1 + settings_postfix_ ) ;
2018-02-27 18:06:05 +01:00
if ( s . value ( " target " ) . isNull ( ) ) {
s . setValue ( " target " , 1 ) ; // 1 == bitrate
}
if ( s . value ( " cbr " ) . isNull ( ) ) {
2019-01-24 19:16:39 +01:00
s . setValue ( " cbr " , false ) ;
2018-02-27 18:06:05 +01:00
}
2022-07-16 16:31:04 +02:00
s . endGroup ( ) ;
2018-02-27 18:06:05 +01:00
}
QList < TranscoderPreset > Transcoder : : GetAllPresets ( ) {
QList < TranscoderPreset > ret ;
2023-02-18 14:09:27 +01:00
ret < < PresetForFileType ( Song : : FileType : : WAV ) ;
ret < < PresetForFileType ( Song : : FileType : : FLAC ) ;
ret < < PresetForFileType ( Song : : FileType : : WavPack ) ;
ret < < PresetForFileType ( Song : : FileType : : OggFlac ) ;
ret < < PresetForFileType ( Song : : FileType : : OggVorbis ) ;
ret < < PresetForFileType ( Song : : FileType : : OggOpus ) ;
ret < < PresetForFileType ( Song : : FileType : : OggSpeex ) ;
ret < < PresetForFileType ( Song : : FileType : : MPEG ) ;
ret < < PresetForFileType ( Song : : FileType : : MP4 ) ;
ret < < PresetForFileType ( Song : : FileType : : ASF ) ;
2022-07-16 16:31:04 +02:00
2018-02-27 18:06:05 +01:00
return ret ;
}
2021-06-20 19:04:08 +02:00
TranscoderPreset Transcoder : : PresetForFileType ( const Song : : FileType filetype ) {
2018-02-27 18:06:05 +01:00
2021-06-20 19:04:08 +02:00
switch ( filetype ) {
2023-02-18 14:09:27 +01:00
case Song : : FileType : : WAV :
2024-04-09 23:20:26 +02:00
return TranscoderPreset ( filetype , QStringLiteral ( " Wav " ) , QStringLiteral ( " wav " ) , QString ( ) , QStringLiteral ( " audio/x-wav " ) ) ;
2023-02-18 14:09:27 +01:00
case Song : : FileType : : FLAC :
2024-04-09 23:20:26 +02:00
return TranscoderPreset ( filetype , QStringLiteral ( " FLAC " ) , QStringLiteral ( " flac " ) , QStringLiteral ( " audio/x-flac " ) ) ;
2023-02-18 14:09:27 +01:00
case Song : : FileType : : WavPack :
2024-04-09 23:20:26 +02:00
return TranscoderPreset ( filetype , QStringLiteral ( " WavPack " ) , QStringLiteral ( " wv " ) , QStringLiteral ( " audio/x-wavpack " ) ) ;
2023-02-18 14:09:27 +01:00
case Song : : FileType : : OggFlac :
2024-04-09 23:20:26 +02:00
return TranscoderPreset ( filetype , QStringLiteral ( " Ogg FLAC " ) , QStringLiteral ( " ogg " ) , QStringLiteral ( " audio/x-flac " ) , QStringLiteral ( " application/ogg " ) ) ;
2023-02-18 14:09:27 +01:00
case Song : : FileType : : OggVorbis :
2024-04-09 23:20:26 +02:00
return TranscoderPreset ( filetype , QStringLiteral ( " Ogg Vorbis " ) , QStringLiteral ( " ogg " ) , QStringLiteral ( " audio/x-vorbis " ) , QStringLiteral ( " application/ogg " ) ) ;
2023-02-18 14:09:27 +01:00
case Song : : FileType : : OggOpus :
2024-04-09 23:20:26 +02:00
return TranscoderPreset ( filetype , QStringLiteral ( " Ogg Opus " ) , QStringLiteral ( " opus " ) , QStringLiteral ( " audio/x-opus " ) , QStringLiteral ( " application/ogg " ) ) ;
2023-02-18 14:09:27 +01:00
case Song : : FileType : : OggSpeex :
2024-04-09 23:20:26 +02:00
return TranscoderPreset ( filetype , QStringLiteral ( " Ogg Speex " ) , QStringLiteral ( " spx " ) , QStringLiteral ( " audio/x-speex " ) , QStringLiteral ( " application/ogg " ) ) ;
2023-02-18 14:09:27 +01:00
case Song : : FileType : : MPEG :
2024-04-09 23:20:26 +02:00
return TranscoderPreset ( filetype , QStringLiteral ( " MP3 " ) , QStringLiteral ( " mp3 " ) , QStringLiteral ( " audio/mpeg, mpegversion=(int)1, layer=(int)3 " ) ) ;
2023-02-18 14:09:27 +01:00
case Song : : FileType : : MP4 :
2024-04-09 23:20:26 +02:00
return TranscoderPreset ( filetype , QStringLiteral ( " M4A AAC " ) , QStringLiteral ( " mp4 " ) , QStringLiteral ( " audio/mpeg, mpegversion=(int)4 " ) , QStringLiteral ( " audio/mp4 " ) ) ;
2023-02-18 14:09:27 +01:00
case Song : : FileType : : ASF :
2024-04-09 23:20:26 +02:00
return TranscoderPreset ( filetype , QStringLiteral ( " Windows Media audio " ) , QStringLiteral ( " wma " ) , QStringLiteral ( " audio/x-wma " ) , QStringLiteral ( " video/x-ms-asf " ) ) ;
2018-02-27 18:06:05 +01:00
default :
2023-02-18 14:09:27 +01:00
qLog ( Warning ) < < " Unsupported format in PresetForFileType: " < < static_cast < int > ( filetype ) ;
2018-02-27 18:06:05 +01:00
return TranscoderPreset ( ) ;
}
}
2021-06-20 19:04:08 +02:00
Song : : FileType Transcoder : : PickBestFormat ( const QList < Song : : FileType > & supported ) {
2018-02-27 18:06:05 +01:00
2023-02-18 14:09:27 +01:00
if ( supported . isEmpty ( ) ) return Song : : FileType : : Unknown ;
2018-02-27 18:06:05 +01:00
2024-04-23 17:15:42 +02:00
const QList < Song : : FileType > best_formats = QList < Song : : FileType > ( ) < < Song : : FileType : : FLAC < < Song : : FileType : : OggFlac < < Song : : FileType : : WavPack ;
2018-02-27 18:06:05 +01:00
for ( Song : : FileType type : best_formats ) {
2023-07-21 07:16:08 +02:00
if ( supported . contains ( type ) ) return type ;
2018-02-27 18:06:05 +01:00
}
return supported [ 0 ] ;
}
2021-06-20 19:04:08 +02:00
QString Transcoder : : GetFile ( const QString & input , const TranscoderPreset & preset , const QString & output ) {
2018-02-27 18:06:05 +01:00
2019-01-26 14:57:25 +01:00
QFileInfo fileinfo_output ;
if ( ! output . isEmpty ( ) ) {
fileinfo_output . setFile ( output ) ;
}
2018-02-27 18:06:05 +01:00
2019-01-26 14:57:25 +01:00
if ( ! fileinfo_output . isFile ( ) | | fileinfo_output . filePath ( ) . isEmpty ( ) | | fileinfo_output . path ( ) . isEmpty ( ) | | fileinfo_output . fileName ( ) . isEmpty ( ) | | fileinfo_output . suffix ( ) . isEmpty ( ) ) {
QFileInfo fileinfo_input ( input ) ;
2024-09-07 04:24:14 +02:00
QString temp_dir = QStandardPaths : : writableLocation ( QStandardPaths : : CacheLocation ) + " /transcoder " _L1 ;
2021-03-20 18:58:38 +01:00
if ( ! QDir ( temp_dir ) . exists ( ) ) QDir ( ) . mkpath ( temp_dir ) ;
2024-04-11 02:56:01 +02:00
QString filename = fileinfo_input . completeBaseName ( ) + QLatin1Char ( ' . ' ) + preset . extension_ ;
fileinfo_output . setFile ( temp_dir + QLatin1Char ( ' / ' ) + filename ) ;
2019-01-26 14:57:25 +01:00
}
2018-02-27 18:06:05 +01:00
// Never overwrite existing files
2019-01-26 14:57:25 +01:00
if ( fileinfo_output . exists ( ) ) {
QString path = fileinfo_output . path ( ) ;
2020-09-29 17:30:21 +02:00
QString filename = fileinfo_output . completeBaseName ( ) ;
2019-01-26 14:57:25 +01:00
QString suffix = fileinfo_output . suffix ( ) ;
2018-02-27 18:06:05 +01:00
for ( int i = 0 ; ; + + i ) {
2024-04-09 23:20:26 +02:00
QString new_filename = QStringLiteral ( " %1/%2-%3.%4 " ) . arg ( path , filename ) . arg ( i ) . arg ( suffix ) ;
2019-01-26 14:57:25 +01:00
fileinfo_output . setFile ( new_filename ) ;
if ( ! fileinfo_output . exists ( ) ) {
2018-02-27 18:06:05 +01:00
break ;
}
2019-01-26 14:57:25 +01:00
2018-02-27 18:06:05 +01:00
}
}
2019-01-26 14:57:25 +01:00
return fileinfo_output . filePath ( ) ;
2022-07-16 16:31:04 +02:00
2018-02-27 18:06:05 +01:00
}
2019-01-26 14:57:25 +01:00
void Transcoder : : AddJob ( const QString & input , const TranscoderPreset & preset , const QString & output ) {
2018-02-27 18:06:05 +01:00
Job job ;
job . input = input ;
job . preset = preset ;
2019-01-26 14:57:25 +01:00
job . output = output ;
2018-02-27 18:06:05 +01:00
queued_jobs_ < < job ;
}
void Transcoder : : Start ( ) {
2024-08-25 01:06:30 +02:00
Q_EMIT LogLine ( tr ( " Transcoding %1 files using %2 threads " ) . arg ( queued_jobs_ . count ( ) ) . arg ( max_threads ( ) ) ) ;
2018-02-27 18:06:05 +01:00
2024-08-25 06:24:02 +02:00
Q_FOREVER {
2018-02-27 18:06:05 +01:00
StartJobStatus status = MaybeStartNextJob ( ) ;
2023-02-18 14:09:27 +01:00
if ( status = = StartJobStatus : : AllThreadsBusy | | status = = StartJobStatus : : NoMoreJobs ) break ;
2018-02-27 18:06:05 +01:00
}
}
Transcoder : : StartJobStatus Transcoder : : MaybeStartNextJob ( ) {
2023-02-18 14:09:27 +01:00
if ( current_jobs_ . count ( ) > = max_threads ( ) ) return StartJobStatus : : AllThreadsBusy ;
2018-02-27 18:06:05 +01:00
if ( queued_jobs_ . isEmpty ( ) ) {
if ( current_jobs_ . isEmpty ( ) ) {
2024-08-25 01:06:30 +02:00
Q_EMIT AllJobsComplete ( ) ;
2018-02-27 18:06:05 +01:00
}
2023-02-18 14:09:27 +01:00
return StartJobStatus : : NoMoreJobs ;
2018-02-27 18:06:05 +01:00
}
Job job = queued_jobs_ . takeFirst ( ) ;
if ( StartJob ( job ) ) {
2023-02-18 14:09:27 +01:00
return StartJobStatus : : StartedSuccessfully ;
2018-02-27 18:06:05 +01:00
}
2024-08-25 01:06:30 +02:00
Q_EMIT JobComplete ( job . input , job . output , false ) ;
2023-02-18 14:09:27 +01:00
return StartJobStatus : : FailedToStart ;
2018-02-27 18:06:05 +01:00
}
void Transcoder : : NewPadCallback ( GstElement * , GstPad * pad , gpointer data ) {
JobState * state = reinterpret_cast < JobState * > ( data ) ;
GstPad * const audiopad = gst_element_get_static_pad ( state - > convert_element_ , " sink " ) ;
if ( GST_PAD_IS_LINKED ( audiopad ) ) {
2023-01-12 23:37:12 +01:00
qLog ( Debug ) < < " Audiopad is already linked, unlinking old pad " ;
2018-02-27 18:06:05 +01:00
gst_pad_unlink ( audiopad , GST_PAD_PEER ( audiopad ) ) ;
}
gst_pad_link ( pad , audiopad ) ;
gst_object_unref ( audiopad ) ;
}
GstBusSyncReply Transcoder : : BusCallbackSync ( GstBus * , GstMessage * msg , gpointer data ) {
JobState * state = reinterpret_cast < JobState * > ( data ) ;
switch ( GST_MESSAGE_TYPE ( msg ) ) {
case GST_MESSAGE_EOS :
state - > PostFinished ( true ) ;
break ;
case GST_MESSAGE_ERROR :
state - > ReportError ( msg ) ;
state - > PostFinished ( false ) ;
break ;
default :
break ;
}
2019-01-24 19:16:39 +01:00
2018-02-27 18:06:05 +01:00
return GST_BUS_PASS ;
}
2021-06-22 13:45:29 +02:00
void Transcoder : : JobState : : ReportError ( GstMessage * msg ) const {
2018-02-27 18:06:05 +01:00
2021-03-26 21:30:13 +01:00
GError * error = nullptr ;
gchar * debugs = nullptr ;
2018-02-27 18:06:05 +01:00
gst_message_parse_error ( msg , & error , & debugs ) ;
QString message = QString : : fromLocal8Bit ( error - > message ) ;
g_error_free ( error ) ;
2022-02-06 18:47:23 +01:00
g_free ( debugs ) ;
2018-02-27 18:06:05 +01:00
2024-08-25 01:06:30 +02:00
Q_EMIT parent_ - > LogLine ( tr ( " Error processing %1: %2 " ) . arg ( QDir : : toNativeSeparators ( job_ . input ) , message ) ) ;
2018-02-27 18:06:05 +01:00
}
bool Transcoder : : StartJob ( const Job & job ) {
2023-07-21 05:55:24 +02:00
SharedPtr < JobState > state = make_shared < JobState > ( job , this ) ;
2018-02-27 18:06:05 +01:00
2024-08-25 01:06:30 +02:00
Q_EMIT LogLine ( tr ( " Starting %1 " ) . arg ( QDir : : toNativeSeparators ( job . input ) ) ) ;
2018-02-27 18:06:05 +01:00
// Create the pipeline.
2018-05-01 00:41:33 +02:00
// This should be a scoped_ptr, but scoped_ptr doesn't support custom destructors.
2018-02-27 18:06:05 +01:00
state - > pipeline_ = gst_pipeline_new ( " pipeline " ) ;
if ( ! state - > pipeline_ ) return false ;
// Create all the elements
2024-04-09 23:20:26 +02:00
GstElement * src = CreateElement ( QStringLiteral ( " filesrc " ) , state - > pipeline_ ) ;
GstElement * decode = CreateElement ( QStringLiteral ( " decodebin " ) , state - > pipeline_ ) ;
GstElement * convert = CreateElement ( QStringLiteral ( " audioconvert " ) , state - > pipeline_ ) ;
GstElement * resample = CreateElement ( QStringLiteral ( " audioresample " ) , state - > pipeline_ ) ;
2024-07-11 09:31:51 +02:00
GstElement * codec = CreateElementForMimeType ( GST_ELEMENT_FACTORY_TYPE_AUDIO_ENCODER , job . preset . codec_mimetype_ , state - > pipeline_ ) ;
GstElement * muxer = CreateElementForMimeType ( GST_ELEMENT_FACTORY_TYPE_MUXER , job . preset . muxer_mimetype_ , state - > pipeline_ ) ;
2024-04-09 23:20:26 +02:00
GstElement * sink = CreateElement ( QStringLiteral ( " filesink " ) , state - > pipeline_ ) ;
2018-02-27 18:06:05 +01:00
if ( ! src | | ! decode | | ! convert | | ! sink ) return false ;
if ( ! codec & & ! job . preset . codec_mimetype_ . isEmpty ( ) ) {
2024-08-25 01:06:30 +02:00
Q_EMIT LogLine ( tr ( " Couldn't find an encoder for %1, check you have the correct GStreamer plugins installed " ) . arg ( job . preset . codec_mimetype_ ) ) ;
2018-02-27 18:06:05 +01:00
return false ;
}
if ( ! muxer & & ! job . preset . muxer_mimetype_ . isEmpty ( ) ) {
2024-08-25 01:06:30 +02:00
Q_EMIT LogLine ( tr ( " Couldn't find a muxer for %1, check you have the correct GStreamer plugins installed " ) . arg ( job . preset . muxer_mimetype_ ) ) ;
2018-02-27 18:06:05 +01:00
return false ;
}
// Join them together
gst_element_link ( src , decode ) ;
if ( codec & & muxer ) gst_element_link_many ( convert , resample , codec , muxer , sink , nullptr ) ;
else if ( codec ) gst_element_link_many ( convert , resample , codec , sink , nullptr ) ;
else if ( muxer ) gst_element_link_many ( convert , resample , muxer , sink , nullptr ) ;
// Set properties
g_object_set ( src , " location " , job . input . toUtf8 ( ) . constData ( ) , nullptr ) ;
g_object_set ( sink , " location " , job . output . toUtf8 ( ) . constData ( ) , nullptr ) ;
// Set callbacks
state - > convert_element_ = convert ;
CHECKED_GCONNECT ( decode , " pad-added " , & NewPadCallback , state . get ( ) ) ;
gst_bus_set_sync_handler ( gst_pipeline_get_bus ( GST_PIPELINE ( state - > pipeline_ ) ) , BusCallbackSync , state . get ( ) , nullptr ) ;
// Start the pipeline
gst_element_set_state ( state - > pipeline_ , GST_STATE_PLAYING ) ;
2018-05-01 00:41:33 +02:00
// GStreamer now transcodes in another thread, so we can return now and do something else.
// Keep the JobState object around. It'll post an event to our event loop when it finishes.
2018-02-27 18:06:05 +01:00
current_jobs_ < < state ;
return true ;
}
Transcoder : : JobState : : ~ JobState ( ) {
if ( pipeline_ ) {
gst_element_set_state ( pipeline_ , GST_STATE_NULL ) ;
gst_object_unref ( pipeline_ ) ;
}
}
bool Transcoder : : event ( QEvent * e ) {
if ( e - > type ( ) = = JobFinishedEvent : : sEventType ) {
JobFinishedEvent * finished_event = static_cast < JobFinishedEvent * > ( e ) ;
// Find this job in the list
JobStateList : : iterator it = current_jobs_ . begin ( ) ;
2021-08-23 21:21:08 +02:00
for ( ; it ! = current_jobs_ . end ( ) ; + + it ) {
2018-02-27 18:06:05 +01:00
if ( it - > get ( ) = = finished_event - > state_ ) break ;
}
if ( it = = current_jobs_ . end ( ) ) {
2018-05-01 00:41:33 +02:00
// Couldn't find it, maybe GStreamer gave us an event after we'd destroyed the pipeline?
2018-02-27 18:06:05 +01:00
return true ;
}
QString input = ( * it ) - > job_ . input ;
QString output = ( * it ) - > job_ . output ;
2022-08-28 02:44:37 +02:00
// Remove event handlers from the gstreamer pipeline, so they don't get called after the pipeline is shutting down
2018-02-27 18:06:05 +01:00
gst_bus_set_sync_handler ( gst_pipeline_get_bus ( GST_PIPELINE ( finished_event - > state_ - > pipeline_ ) ) , nullptr , nullptr , nullptr ) ;
// Remove it from the list - this will also destroy the GStreamer pipeline
2024-08-23 20:30:59 +02:00
current_jobs_ . erase ( it ) ;
2018-02-27 18:06:05 +01:00
// Emit the finished signal
2024-08-25 01:06:30 +02:00
Q_EMIT JobComplete ( input , output , finished_event - > success_ ) ;
2018-02-27 18:06:05 +01:00
// Start some more jobs
MaybeStartNextJob ( ) ;
return true ;
}
return QObject : : event ( e ) ;
}
void Transcoder : : Cancel ( ) {
// Remove all pending jobs
queued_jobs_ . clear ( ) ;
// Stop the running ones
JobStateList : : iterator it = current_jobs_ . begin ( ) ;
while ( it ! = current_jobs_ . end ( ) ) {
2023-07-21 05:55:24 +02:00
SharedPtr < JobState > state ( * it ) ;
2018-02-27 18:06:05 +01:00
2022-08-28 02:44:37 +02:00
// Remove event handlers from the gstreamer pipeline, so they don't get called after the pipeline is shutting down
2018-02-27 18:06:05 +01:00
gst_bus_set_sync_handler ( gst_pipeline_get_bus ( GST_PIPELINE ( state - > pipeline_ ) ) , nullptr , nullptr , nullptr ) ;
// Stop the pipeline
if ( gst_element_set_state ( state - > pipeline_ , GST_STATE_NULL ) = = GST_STATE_CHANGE_ASYNC ) {
// Wait for it to finish stopping...
gst_element_get_state ( state - > pipeline_ , nullptr , nullptr , GST_CLOCK_TIME_NONE ) ;
}
// Remove the job, this destroys the GStreamer pipeline too
2024-08-23 20:30:59 +02:00
it = current_jobs_ . erase ( it ) ;
2018-02-27 18:06:05 +01:00
}
}
QMap < QString , float > Transcoder : : GetProgress ( ) const {
QMap < QString , float > ret ;
for ( const auto & state : current_jobs_ ) {
if ( ! state - > pipeline_ ) continue ;
gint64 position = 0 ;
gint64 duration = 0 ;
gst_element_query_position ( state - > pipeline_ , GST_FORMAT_TIME , & position ) ;
gst_element_query_duration ( state - > pipeline_ , GST_FORMAT_TIME , & duration ) ;
2021-10-30 02:21:29 +02:00
ret [ state - > job_ . input ] = static_cast < float > ( position ) / static_cast < float > ( duration ) ;
2018-02-27 18:06:05 +01:00
}
return ret ;
}
void Transcoder : : SetElementProperties ( const QString & name , GObject * object ) {
2024-04-11 02:56:01 +02:00
Settings s ;
2024-09-07 04:24:14 +02:00
s . beginGroup ( " Transcoder/ " _L1 + name + settings_postfix_ ) ;
2018-02-27 18:06:05 +01:00
guint properties_count = 0 ;
2019-01-06 14:34:50 +01:00
GParamSpec * * properties = g_object_class_list_properties ( G_OBJECT_GET_CLASS ( object ) , & properties_count ) ;
2018-02-27 18:06:05 +01:00
2019-11-15 00:23:06 +01:00
for ( uint i = 0 ; i < properties_count ; + + i ) {
2018-02-27 18:06:05 +01:00
GParamSpec * property = properties [ i ] ;
2024-04-11 02:56:01 +02:00
if ( ! s . contains ( QString : : fromUtf8 ( property - > name ) ) ) {
2023-01-12 23:37:12 +01:00
continue ;
}
2024-04-11 02:56:01 +02:00
const QVariant value = s . value ( QString : : fromUtf8 ( property - > name ) ) ;
2023-01-12 23:37:12 +01:00
if ( value . isNull ( ) ) {
continue ;
}
2018-02-27 18:06:05 +01:00
2024-08-25 01:06:30 +02:00
Q_EMIT LogLine ( QStringLiteral ( " Setting %1 property: %2 = %3 " ) . arg ( name , QString : : fromUtf8 ( property - > name ) , value . toString ( ) ) ) ;
2018-02-27 18:06:05 +01:00
switch ( property - > value_type ) {
2023-01-12 23:37:12 +01:00
case G_TYPE_FLOAT : {
const double g_value = static_cast < gfloat > ( value . toFloat ( ) ) ;
qLog ( Debug ) < < " Setting " < < property - > name < < " float " < < " to " < < g_value ;
g_object_set ( object , property - > name , g_value , nullptr ) ;
2018-02-27 18:06:05 +01:00
break ;
2023-01-12 23:37:12 +01:00
}
case G_TYPE_DOUBLE : {
const double g_value = static_cast < gdouble > ( value . toDouble ( ) ) ;
qLog ( Debug ) < < " Setting " < < property - > name < < " (double) " < < " to " < < g_value ;
g_object_set ( object , property - > name , g_value , nullptr ) ;
2018-02-27 18:06:05 +01:00
break ;
2023-01-12 23:37:12 +01:00
}
case G_TYPE_BOOLEAN : {
const bool g_value = value . toBool ( ) ;
qLog ( Debug ) < < " Setting " < < property - > name < < " (bool) " < < " to " < < g_value ;
g_object_set ( object , property - > name , g_value , nullptr ) ;
2018-02-27 18:06:05 +01:00
break ;
2023-01-12 23:37:12 +01:00
}
2018-02-27 18:06:05 +01:00
case G_TYPE_INT :
2023-01-12 23:37:12 +01:00
case G_TYPE_ENUM : {
const gint g_value = static_cast < gint > ( value . toInt ( ) ) ;
qLog ( Debug ) < < " Setting " < < property - > name < < " (enum) " < < " to " < < g_value ;
g_object_set ( object , property - > name , g_value , nullptr ) ;
break ;
}
case G_TYPE_UINT : {
const guint g_value = static_cast < gint > ( value . toUInt ( ) ) ;
qLog ( Debug ) < < " Setting " < < property - > name < < " (uint) " < < " to " < < g_value ;
g_object_set ( object , property - > name , g_value , nullptr ) ;
break ;
}
case G_TYPE_LONG :
case G_TYPE_INT64 : {
const glong g_value = static_cast < glong > ( value . toLongLong ( ) ) ;
qLog ( Debug ) < < " Setting " < < property - > name < < " (long) " < < " to " < < g_value ;
g_object_set ( object , property - > name , g_value , nullptr ) ;
2018-02-27 18:06:05 +01:00
break ;
2023-01-12 23:37:12 +01:00
}
case G_TYPE_ULONG :
case G_TYPE_UINT64 : {
const gulong g_value = static_cast < gulong > ( value . toULongLong ( ) ) ;
qLog ( Debug ) < < " Setting " < < property - > name < < " (ulong) " < < " to " < < g_value ;
g_object_set ( object , property - > name , g_value , nullptr ) ;
break ;
}
default : {
const gint g_value = static_cast < gint > ( value . toInt ( ) ) ;
qLog ( Debug ) < < " Setting " < < property - > name < < " (int) " < < " to " < < g_value ;
g_object_set ( object , property - > name , g_value , nullptr ) ;
break ;
}
2018-02-27 18:06:05 +01:00
}
}
g_free ( properties ) ;
2022-07-16 16:31:04 +02:00
s . endGroup ( ) ;
2018-02-27 18:06:05 +01:00
}