From 14d0b00f46fbde43a772e8846e76701551ace512 Mon Sep 17 00:00:00 2001 From: David Sansome Date: Sun, 11 Apr 2010 19:47:21 +0000 Subject: [PATCH] Factor out the gstreamer pipeline bits into another class. --- src/CMakeLists.txt | 2 + src/engines/gstengine.cpp | 554 +++++------------------------- src/engines/gstengine.h | 85 +---- src/engines/gstenginepipeline.cpp | 299 ++++++++++++++++ src/engines/gstenginepipeline.h | 99 ++++++ 5 files changed, 503 insertions(+), 536 deletions(-) create mode 100644 src/engines/gstenginepipeline.cpp create mode 100644 src/engines/gstenginepipeline.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index cca9105c2..7c2e4a394 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -14,6 +14,7 @@ set(CLEMENTINE-SOURCES engines/enginebase.cpp engines/gstengine.cpp engines/gstequalizer.cpp + engines/gstenginepipeline.cpp analyzers/baranalyzer.cpp analyzers/analyzerbase.cpp fht.cpp @@ -84,6 +85,7 @@ set(CLEMENTINE-MOC-HEADERS playlist.h engines/enginebase.h engines/gstengine.h + engines/gstenginepipeline.h sliderwidget.h playlistview.h backgroundthread.h diff --git a/src/engines/gstengine.cpp b/src/engines/gstengine.cpp index 2c17d578b..656a29200 100644 --- a/src/engines/gstengine.cpp +++ b/src/engines/gstengine.cpp @@ -23,6 +23,7 @@ #include "gstengine.h" #include "gstequalizer.h" +#include "gstenginepipeline.h" #include #include @@ -39,157 +40,18 @@ #include #include -#define RETURN_IF_PIPELINE_EMPTY if ( !pipeline_filled_ ) return - using std::vector; +using boost::shared_ptr; -GstEngine* GstEngine::sInstance = NULL; const char* GstEngine::kSettingsGroup = "GstEngine"; const char* GstEngine::kAutoSink = "autoaudiosink"; -///////////////////////////////////////////////////////////////////////////////////// -// CALLBACKS -///////////////////////////////////////////////////////////////////////////////////// - -//GstBusSyncReply -gboolean GstEngine::BusCallback(GstBus*, GstMessage* msg, gpointer) { - switch ( GST_MESSAGE_TYPE(msg)) { - case GST_MESSAGE_ERROR: { - GError* error; - gchar* debugs; - - gst_message_parse_error(msg,&error,&debugs); - qDebug() << "ERROR RECEIVED IN BUS_CB <" << error->message << ">" ;; - - instance()->gst_error_ = QString::fromAscii( error->message ); - instance()->gst_debug_ = QString::fromAscii( debugs ); - QMetaObject::invokeMethod(instance(), "HandlePipelineError", Qt::QueuedConnection); - break; - } - - case GST_MESSAGE_TAG: { - gchar* string=NULL; - Engine::SimpleMetaBundle bundle; - GstTagList* taglist; - gst_message_parse_tag(msg,&taglist); - bool success = false; - - if ( gst_tag_list_get_string( taglist, GST_TAG_TITLE, &string ) && string ) { - qDebug() << "received tag 'Title': " << QString( string ) ; - bundle.title = string; - success = true; - } - if ( gst_tag_list_get_string( taglist, GST_TAG_ARTIST, &string ) && string ) { - qDebug() << "received tag 'Artist': " << QString( string ) ; - bundle.artist = string; - success = true; - } - if ( gst_tag_list_get_string( taglist, GST_TAG_COMMENT, &string ) && string ) { - qDebug() << "received tag 'Comment': " << QString( string ) ; - bundle.comment = string; - success = true; - } - if ( gst_tag_list_get_string( taglist, GST_TAG_ALBUM, &string ) && string ) { - qDebug() << "received tag 'Album': " << QString( string ) ; - bundle.album = string; - success = true; - } - g_free(string); - gst_tag_list_free(taglist); - if (success) { - instance()->meta_bundle_ = bundle; - QMetaObject::invokeMethod(instance(), "NewMetaData", Qt::QueuedConnection); - } - break; - } - - default: - break; - } - return GST_BUS_DROP; -} - -GstBusSyncReply GstEngine::BusCallbackSync(GstBus*, GstMessage* msg, gpointer) { - switch (GST_MESSAGE_TYPE(msg)) { - case GST_MESSAGE_EOS: - QMetaObject::invokeMethod(instance(), "EndOfStreamReached", - Qt::QueuedConnection); - break; - - default: - break; - } - - return GST_BUS_PASS; -} - - -void GstEngine::NewPadCallback(GstElement*, GstPad* pad, gboolean, gpointer) { - GstPad* const audiopad = gst_element_get_pad( instance()->gst_audiobin_, "sink" ); - - if ( GST_PAD_IS_LINKED( audiopad ) ) { - qDebug() << "audiopad is already linked. Unlinking old pad." ; - gst_pad_unlink( audiopad, GST_PAD_PEER( audiopad ) ); - } - - gst_pad_link( pad, audiopad ); - - gst_object_unref( audiopad ); -} - - -void GstEngine::HandoffCallback(GstPad*, GstBuffer* buf, gpointer arg) { - GstEngine *thisObj = static_cast( arg ); - - // push the buffer onto the delay queue - gst_buffer_ref(buf); - g_queue_push_tail(thisObj->delayq_, buf); -} - -void GstEngine::EventCallback(GstPad*, GstEvent* event, gpointer) { - - switch ( static_cast(event->type) ) - { - case GST_EVENT_EOS: - qDebug() << "EOS reached"; - QMetaObject::invokeMethod(instance(), "EndOfStreamReached", - Qt::QueuedConnection); - break; - - case GST_EVENT_TAG: - qDebug() << "GOT NEW TAG"; - break; - - default: - break; - } -} - - -void GstEngine::CanDecodeNewPadCallback(GstElement*, GstPad* pad, gboolean, gpointer) { - GstCaps* caps = gst_pad_get_caps(pad); - if (gst_caps_get_size(caps) > 0) { - GstStructure* str = gst_caps_get_structure(caps, 0); - if (g_strrstr(gst_structure_get_name( str ), "audio" )) - instance()->can_decode_success_ = true; - } - gst_caps_unref(caps); -} - -void GstEngine::CanDecodeLastCallback(GstElement*, gpointer) { - instance()->can_decode_last_ = true; -} - - GstEngine::GstEngine() : Engine::Base(), - event_cb_id_(0), delayq_(g_queue_new()), current_sample_(0), - pipeline_filled_(false), - fade_value_(0.0), equalizer_enabled_(false), shutdown_(false), can_decode_pipeline_(NULL), @@ -200,16 +62,13 @@ GstEngine::GstEngine() } GstEngine::~GstEngine() { - DestroyPipeline(); + current_pipeline_.reset(); if (can_decode_pipeline_) gst_object_unref(GST_OBJECT(can_decode_pipeline_)); -#ifdef GST_KIOSTREAMS - delete[] m_streamBuf; -#endif - // Destroy scope delay queue + ClearScopeQ(); g_queue_free(delayq_); // Save configuration @@ -218,8 +77,6 @@ GstEngine::~GstEngine() { bool GstEngine::init() { - sInstance = this; - // GStreamer initialization GError *err; if ( !gst_init_check( NULL, NULL, &err ) ) { @@ -264,8 +121,8 @@ bool GstEngine::canDecode(const QUrl &url) { can_decode_bin_ = CreateElement("decodebin", can_decode_pipeline_); gst_element_link(can_decode_src_, can_decode_bin_); - g_signal_connect(G_OBJECT(can_decode_bin_), "new-decoded-pad", G_CALLBACK(CanDecodeNewPadCallback), NULL); - g_signal_connect(G_OBJECT(can_decode_bin_), "no-more-pads", G_CALLBACK(CanDecodeLastCallback), NULL); + g_signal_connect(G_OBJECT(can_decode_bin_), "new-decoded-pad", G_CALLBACK(CanDecodeNewPadCallback), this); + g_signal_connect(G_OBJECT(can_decode_bin_), "no-more-pads", G_CALLBACK(CanDecodeLastCallback), this); } // Set the file we're testing @@ -287,43 +144,44 @@ bool GstEngine::canDecode(const QUrl &url) { return can_decode_success_; } +void GstEngine::CanDecodeNewPadCallback(GstElement*, GstPad* pad, gboolean, gpointer self) { + GstEngine* instance = static_cast(self); -uint GstEngine::position() const { - RETURN_IF_PIPELINE_EMPTY 0; + GstCaps* caps = gst_pad_get_caps(pad); + if (gst_caps_get_size(caps) > 0) { + GstStructure* str = gst_caps_get_structure(caps, 0); + if (g_strrstr(gst_structure_get_name( str ), "audio" )) + instance->can_decode_success_ = true; + } + gst_caps_unref(caps); +} - GstFormat fmt = GST_FORMAT_TIME; - // Value will hold the current time position in nanoseconds. Must be initialized! - gint64 value = 0; - gst_element_query_position( gst_pipeline_, &fmt, &value ); - - return static_cast( ( value / GST_MSECOND ) ); // nanosec -> msec +void GstEngine::CanDecodeLastCallback(GstElement*, gpointer self) { + GstEngine* instance = static_cast(self); + instance->can_decode_last_ = true; } +uint GstEngine::position() const { + if (!current_pipeline_) + return 0; + + return uint(current_pipeline_->position() / GST_MSECOND); +} + uint GstEngine::length() const { - RETURN_IF_PIPELINE_EMPTY 0; + if (!current_pipeline_) + return 0; - GstFormat fmt = GST_FORMAT_TIME; - // Value will hold the track length in nanoseconds. Must be initialized! - gint64 value = 0; - gst_element_query_duration(gst_pipeline_, &fmt, &value); - - return uint( value / GST_MSECOND ); // nanosec -> msec + return uint(current_pipeline_->length() / GST_MSECOND); } Engine::State GstEngine::state() const { - RETURN_IF_PIPELINE_EMPTY m_url.isEmpty() ? Engine::Empty : Engine::Idle; + if (!current_pipeline_) + return m_url.isEmpty() ? Engine::Empty : Engine::Idle; - GstState s, sp; - GstStateChangeReturn sret = gst_element_get_state( gst_pipeline_, &s, &sp, kGstStateTimeout); - - if (sret == GST_STATE_CHANGE_FAILURE) { - qDebug() << "Gst get state fails"; - return Engine::Empty; - } - - switch (s) { + switch (current_pipeline_->state()) { case GST_STATE_NULL: return Engine::Empty; case GST_STATE_READY: return Engine::Idle; case GST_STATE_PLAYING: return Engine::Playing; @@ -332,6 +190,9 @@ Engine::State GstEngine::state() const { } } +void GstEngine::NewBuffer(GstBuffer* buf) { + g_queue_push_tail(delayq_, buf); +} const Engine::Scope& GstEngine::scope() { UpdateScope(); @@ -422,129 +283,14 @@ void GstEngine::UpdateScope() { } } - -bool GstEngine::metaDataForUrl(const QUrl &url, Engine::SimpleMetaBundle& b) { - qDebug() << "GstEngine::metaDataForUrl " << url ; - // TODO - /*if ( url.scheme() == "cdda" ) - { - // TODO: gstreamer doesn't support cddb natively, but could perhaps use libkcddb? - b.title = QString( "Track %1" ).arg( url.host() ); - b.album = "AudioCD"; - - if ( setupAudioCD( url.encodedQuery().remove( QRegExp( "^\\?" ) ), url.host().toUInt(), true ) ) - { - GstPad *pad; - if ( ( pad = gst_element_get_pad( gst_src_, "src" ) ) ) - { - GstCaps *caps; - if ( ( caps = gst_pad_get_caps( pad ) ) ) - { - GstStructure *structure; - if ( ( structure = gst_caps_get_structure( GST_CAPS(caps), 0 ) ) ) - { - gint channels, rate, width; - gst_structure_get_int( structure, "channels", &channels ); - gst_structure_get_int( structure, "rate", &rate ); - gst_structure_get_int( structure, "width", &width ); - b.bitrate = ( rate * width * channels ) / 1000; - b.samplerate = rate; - } - gst_caps_unref( caps ); - } - - GstQuery *query; - if ( ( query = gst_query_new_duration( GST_FORMAT_TIME ) ) ) - { - if ( gst_pad_query( pad, query )) { - gint64 time; - - gst_query_parse_duration( query, NULL, &time ); - b.length = QString::number( time / GST_SECOND ); - } - gst_query_unref( query ); - } - } - gst_object_unref( GST_OBJECT( pad ) ); - if ( !gst_element_set_state( gst_pipeline_, GST_STATE_NULL ) ) { - qWarning() << "Could not set thread to NULL."; - DestroyPipeline(); - } - } - return true; - }*/ - return false; -} - - -bool GstEngine::getAudioCDContents(const QString &device, QList &urls) { - // TODO - /*qDebug() << "GstEngine::getAudioCDContents " << device ; - - bool result = false; - if ( setupAudioCD( device, 0, true ) ) - { - GstFormat format; - if ( ( format = gst_format_get_by_nick("track") ) != GST_FORMAT_UNDEFINED ) - { - gint64 tracks = 0; - if ( gst_element_query_duration( gst_pipeline_, &format, &tracks ) ) - { - qDebug() << "Found " << tracks << " cdda tracks" ; - for ( int i = 1; i <= tracks; ++i ) - { - QUrl temp( QString( "cdda://%1" ).arg( i ) ); - if ( !device.isNull() ) - temp.setQuery( device ); - urls << temp; - } - result = true; - } - } - if ( !gst_element_set_state( gst_pipeline_, GST_STATE_NULL ) ) { - qWarning() << "Could not set thread to NULL."; - DestroyPipeline(); - } - } - return result;*/ - return false; -} - bool GstEngine::load(const QUrl& url, bool stream) { Engine::Base::load( url, stream ); - qDebug() << "Loading url: " << url.toEncoded() ; - - if ( url.scheme() == "cdda" ) { - /*if ( !setupAudioCD( url.encodedQuery().remove( QRegExp( "^\\?" ) ), url.host().toUInt(), false ) )*/ + shared_ptr pipeline(CreatePipeline(url)); + if (!pipeline) return false; - } else { - if ( !CreatePipeline() ) - return false; - gst_src_ = CreateElement("giosrc"); - if (!gst_src_) { - qDebug() << "******* cannot get stream src " ; - - DestroyPipeline(); - return false; - } - - g_object_set (G_OBJECT (gst_src_), "location", url.toEncoded().constData(), NULL); - gst_bin_add( GST_BIN( gst_pipeline_ ), gst_src_ ); - - if ( !( gst_decodebin_ = CreateElement( "decodebin", gst_pipeline_ ) ) ) { DestroyPipeline(); return false; } - g_signal_connect( G_OBJECT( gst_decodebin_ ), "new-decoded-pad", G_CALLBACK( NewPadCallback ), NULL ); - - GstPad* p = gst_element_get_pad (gst_decodebin_, "sink"); - if (p) { - event_cb_id_ = gst_pad_add_event_probe (p, G_CALLBACK(EventCallback), this); - gst_object_unref (p); - } - - // Link elements. The link from decodebin to audioconvert will be made in the newPad-callback - gst_element_link( gst_src_, gst_decodebin_ ); - } + current_pipeline_ = pipeline; setVolume(m_volume); setEqualizerEnabled(equalizer_enabled_); @@ -555,10 +301,9 @@ bool GstEngine::load(const QUrl& url, bool stream) { bool GstEngine::play( uint offset ) { // Try to play input pipeline; if fails, destroy input bin - GstStateChangeReturn sret; - if ( !(sret = gst_element_set_state( gst_pipeline_, GST_STATE_PLAYING )) ) { + if (!current_pipeline_->SetState(GST_STATE_PLAYING)) { qWarning() << "Could not set thread to PLAYING."; - DestroyPipeline(); + current_pipeline_.reset(); return false; } @@ -575,50 +320,48 @@ bool GstEngine::play( uint offset ) { void GstEngine::stop() { m_url = QUrl(); // To ensure we return Empty from state() - if (pipeline_filled_) { - // Is a fade running? - if ( fade_value_ == 0.0 ) - fade_value_ = 1.0; - else - DestroyPipeline(); - } - + current_pipeline_.reset(); emit stateChanged(Engine::Empty); } void GstEngine::pause() { - RETURN_IF_PIPELINE_EMPTY; + if (!current_pipeline_) + return; - if ( GST_STATE( gst_pipeline_ ) == GST_STATE_PLAYING ) { - gst_element_set_state( gst_pipeline_, GST_STATE_PAUSED ); - emit stateChanged( Engine::Paused ); + if ( current_pipeline_->state() == GST_STATE_PLAYING ) { + current_pipeline_->SetState(GST_STATE_PAUSED); + emit stateChanged(Engine::Paused); } } void GstEngine::unpause() { - RETURN_IF_PIPELINE_EMPTY; + if (!current_pipeline_) + return; - if ( GST_STATE( gst_pipeline_ ) == GST_STATE_PAUSED ) { - gst_element_set_state( gst_pipeline_, GST_STATE_PLAYING ); - emit stateChanged( Engine::Playing ); + if ( current_pipeline_->state() == GST_STATE_PAUSED ) { + current_pipeline_->SetState(GST_STATE_PLAYING); + emit stateChanged(Engine::Playing); } } void GstEngine::seek( uint ms ) { - RETURN_IF_PIPELINE_EMPTY; + if (!current_pipeline_) + return; - if (!gst_element_seek(gst_pipeline_, 1.0, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, GST_SEEK_TYPE_SET, ms*GST_MSECOND, - GST_SEEK_TYPE_NONE, GST_CLOCK_TIME_NONE)) qDebug() << "Seek failed" ; - else ClearScopeQ(); - gst_element_get_state(gst_pipeline_, NULL, NULL, 100*GST_MSECOND); + if (current_pipeline_->Seek(ms * GST_MSECOND)) + ClearScopeQ(); + else + qDebug() << "Seek failed"; + + // ?? + //gst_element_get_state(gst_pipeline_, NULL, NULL, 100*GST_MSECOND); } void GstEngine::setEqualizerEnabled(bool enabled) { equalizer_enabled_= enabled; - RETURN_IF_PIPELINE_EMPTY; - - g_object_set(G_OBJECT(gst_equalizer_), "active", enabled, NULL); + if (current_pipeline_) + current_pipeline_->SetEqualizerEnabled(enabled); } @@ -626,30 +369,13 @@ void GstEngine::setEqualizerParameters( int preamp, const QList& band_gains equalizer_preamp_ = preamp; equalizer_gains_ = band_gains; - RETURN_IF_PIPELINE_EMPTY; - - // Preamp - g_object_set(G_OBJECT(gst_equalizer_), "preamp", ( preamp + 100 ) / 2, NULL); - - // Gains - vector gains_temp; - gains_temp.resize( band_gains.count() ); - for ( int i = 0; i < band_gains.count(); i++ ) - gains_temp[i] = band_gains.at( i ) + 100; - - g_object_set(G_OBJECT(gst_equalizer_), "gain", &gains_temp, NULL); + if (current_pipeline_) + current_pipeline_->SetEqualizerParams(preamp, band_gains); } void GstEngine::setVolumeSW( uint percent ) { - RETURN_IF_PIPELINE_EMPTY; - - double fade; - if ( fade_value_ > 0.0 ) - fade = 1.0 - log10( ( 1.0 - fade_value_ ) * 9.0 + 1.0 ); - else - fade = 1.0; - - g_object_set( G_OBJECT(gst_volume_), "volume", (double) percent * fade * 0.01, NULL ); + if (current_pipeline_) + current_pipeline_->SetVolume(percent); } @@ -662,7 +388,7 @@ void GstEngine::timerEvent( QTimerEvent* ) { // *** Volume fading *** // Are we currently fading? - if ( fade_value_ > 0.0 ) { + /*if ( fade_value_ > 0.0 ) { // TODO //m_fadeValue -= ( AmarokConfig::fadeoutLength() ) ? 1.0 / AmarokConfig::fadeoutLength() * TIMER_INTERVAL : 1.0; fade_value_ -= 1.0; @@ -675,7 +401,7 @@ void GstEngine::timerEvent( QTimerEvent* ) { //killTimers(); } setVolume( volume() ); - } + }*/ } @@ -683,34 +409,20 @@ void GstEngine::timerEvent( QTimerEvent* ) { // PRIVATE SLOTS ///////////////////////////////////////////////////////////////////////////////////// -void GstEngine::HandlePipelineError() { - QString text = "[GStreamer Error] "; - text += gst_error_; +void GstEngine::HandlePipelineError(const QString& message) { + qDebug() << "Gstreamer error:" << message; - if ( !gst_debug_.isEmpty() ) { - text += " ** "; - text += gst_debug_; - } - - gst_error_ = QString(); - emit statusText( text ); - qWarning() << text ; - - DestroyPipeline(); + current_pipeline_.reset(); } void GstEngine::EndOfStreamReached() { - DestroyPipeline(); + current_pipeline_.reset(); emit trackEnded(); } -void GstEngine::NewMetaData() { - emit metaData( meta_bundle_ ); -} - -void GstEngine::ErrorNoOutput() { - QMessageBox::information( 0, "Error", "

Please select a GStreamer output plugin in the engine settings dialog.

" ); +void GstEngine::NewMetaData(const Engine::SimpleMetaBundle& bundle) { + emit metaData(bundle); } GstElement* GstEngine::CreateElement( @@ -759,116 +471,30 @@ GstEngine::PluginDetailsList return ret; } +shared_ptr GstEngine::CreatePipeline(const QUrl& url) { + shared_ptr ret(new GstEnginePipeline); + ret->set_forwards_buffers(true); + ret->set_output_device(sink_, device_); -bool GstEngine::CreatePipeline() { - DestroyPipeline(); + connect(ret.get(), SIGNAL(EndOfStreamReached()), SLOT(EndOfStreamReached())); + connect(ret.get(), SIGNAL(BufferFound(GstBuffer*)), SLOT(NewBuffer(GstBuffer*))); + connect(ret.get(), SIGNAL(Error(QString)), SLOT(HandlePipelineError(QString))); + connect(ret.get(), SIGNAL(MetadataFound(Engine::SimpleMetaBundle)), + SLOT(NewMetaData(Engine::SimpleMetaBundle))); + connect(ret.get(), SIGNAL(destroyed()), SLOT(ClearScopeQ())); - gst_pipeline_ = gst_pipeline_new( "pipeline" ); - gst_audiobin_ = gst_bin_new( "audiobin" ); + if (!ret->Init(url)) + ret.reset(); - if ( !( gst_audiosink_ = CreateElement( sink_, gst_audiobin_ ) ) ) { - QMetaObject::invokeMethod(this, "ErrorNoOutput", Qt::QueuedConnection); - return false; - } - - if (DoesThisSinkSupportChangingTheOutputDeviceToAUserEditableString(sink_) && !device_.isEmpty()) { - g_object_set(G_OBJECT(gst_audiosink_), "device", device_.toUtf8().constData(), NULL); - } - - gst_equalizer_ = GST_ELEMENT(gst_equalizer_new()); - gst_bin_add(GST_BIN(gst_audiobin_), gst_equalizer_); - - if ( !( gst_audioconvert_ = CreateElement( "audioconvert", gst_audiobin_ ) ) ) { return false; } - if ( !( gst_identity_ = CreateElement( "identity", gst_audiobin_ ) ) ) { return false; } - if ( !( gst_volume_ = CreateElement( "volume", gst_audiobin_ ) ) ) { return false; } - if ( !( gst_audioscale_ = CreateElement( "audioresample", gst_audiobin_ ) ) ) { return false; } - - GstPad* p; - p = gst_element_get_pad(gst_audioconvert_, "sink"); - gst_element_add_pad(gst_audiobin_,gst_ghost_pad_new("sink",p)); - gst_object_unref(p); - - // add a data probe on the src pad if the audioconvert element for our scope - // we do it here because we want pre-equalized and pre-volume samples - // so that our visualization are not affected by them - p = gst_element_get_pad (gst_audioconvert_, "src"); - gst_pad_add_buffer_probe (p, G_CALLBACK(HandoffCallback), this); - gst_object_unref (p); - - // Ensure we get the right type out of audioconvert for our scope - GstCaps* caps = gst_caps_new_simple ("audio/x-raw-int", - "width", G_TYPE_INT, 16, - "signed", G_TYPE_BOOLEAN, true, - NULL); - gst_element_link_filtered(gst_audioconvert_, gst_equalizer_, caps); - gst_caps_unref(caps); - - // Add an extra audioconvert at the end as osxaudiosink supports only one format. - GstElement* convert = CreateElement( "audioconvert", gst_audiobin_, "FFFUUUU" ); - if (!convert) { return false; } - gst_element_link_many( gst_equalizer_, gst_identity_, gst_volume_, - gst_audioscale_, convert, gst_audiosink_, NULL ); - - - gst_bin_add( GST_BIN(gst_pipeline_), gst_audiobin_); - gst_bus_set_sync_handler(gst_pipeline_get_bus(GST_PIPELINE(gst_pipeline_)), BusCallbackSync, NULL); - gst_bus_add_watch(gst_pipeline_get_bus(GST_PIPELINE(gst_pipeline_)), BusCallback, NULL); - - pipeline_filled_ = true; - return true; + return ret; } - -void GstEngine::DestroyPipeline() { - fade_value_ = 0.0; - - ClearScopeQ(); - - if ( pipeline_filled_ ) { - // Remove the event handler while we destroy the pipeline - GstPad *p = gst_element_get_pad (gst_decodebin_, "sink"); - if (p) - gst_pad_remove_event_probe(p, event_cb_id_); - - gst_element_set_state( gst_pipeline_, GST_STATE_NULL ); - gst_object_unref( GST_OBJECT( gst_pipeline_ ) ); - - pipeline_filled_ = false; - } -} - - -bool GstEngine::SetupAudioCD( const QString& device, unsigned track, bool pause) { - qDebug() << "setupAudioCD: device = " << device << ", track = " << track << ", pause = " << pause ; - bool filled = pipeline_filled_ && gst_src_ && strcmp( gst_element_get_name( gst_src_ ), "cdiocddasrc" ) == 0; - if ( filled || CreatePipeline() ) { - if ( filled || ( gst_src_ = CreateElement( "cdiocddasrc", gst_pipeline_, "cdiocddasrc" ) ) ) { - // TODO: allow user to configure default device rather than falling back to gstreamer default when no device passed in - if ( !device.isNull() ) - g_object_set( G_OBJECT(gst_src_), "device", device.toAscii().constData(), NULL ); - if ( track ) - g_object_set (G_OBJECT (gst_src_), "track", track, NULL); - if ( filled || gst_element_link( gst_src_, gst_audiobin_ ) ) { - // the doco says we should only have to go to READY to read metadata, but that doesn't actually work - if ( gst_element_set_state( gst_pipeline_, pause ? GST_STATE_PAUSED : GST_STATE_READY ) != GST_STATE_CHANGE_FAILURE && gst_element_get_state( gst_pipeline_, NULL, NULL, GST_CLOCK_TIME_NONE ) == GST_STATE_CHANGE_SUCCESS ) - { - return true; - } - } - } - DestroyPipeline(); - } - return false; -} - - qint64 GstEngine::PruneScope() { - if ( !pipeline_filled_ ) return 0; // don't prune if we aren't playing + if (!current_pipeline_) + return 0; // get the position playing in the audio device - GstFormat fmt = GST_FORMAT_TIME; - gint64 pos = 0; - gst_element_query_position( gst_pipeline_, &fmt, &pos ); + gint64 pos = current_pipeline_->position(); GstBuffer *buf = 0; quint64 etime; diff --git a/src/engines/gstengine.h b/src/engines/gstengine.h index bb26050aa..0e0312ffa 100644 --- a/src/engines/gstengine.h +++ b/src/engines/gstengine.h @@ -31,17 +31,16 @@ #include #include +#include class QTimerEvent; +class GstEnginePipeline; + /** * @class GstEngine * @short GStreamer engine plugin * @author Mark Kretschmann - * - * GstEngine uses following pipeline for playing (syntax used by gst-launch): - * { filesrc location=file.ext ! decodebin ! audioconvert ! audioscale ! volume - * ! adder } ! { queue ! equalizer ! identity ! volume ! audiosink } */ class GstEngine : public Engine::Base { Q_OBJECT @@ -69,15 +68,15 @@ class GstEngine : public Engine::Base { Engine::State state() const; const Engine::Scope& scope(); - virtual bool metaDataForUrl(const QUrl &url, Engine::SimpleMetaBundle &b); - virtual bool getAudioCDContents(const QString &device, QList &urls); - void gstStatusText(const QString& str) { emit statusText( str ); } void gstMetaData(Engine::SimpleMetaBundle &bundle) { emit metaData( bundle ); } PluginDetailsList GetOutputsList() const { return GetPluginList( "Sink/Audio" ); } static bool DoesThisSinkSupportChangingTheOutputDeviceToAUserEditableString(const QString& name); + static GstElement* CreateElement( + const QString& factoryName, GstElement* bin = 0, const QString& name = 0); + public slots: bool load(const QUrl&, bool stream); bool play(uint offset); @@ -99,54 +98,22 @@ class GstEngine : public Engine::Base { void timerEvent(QTimerEvent*); private slots: - void HandlePipelineError(); void EndOfStreamReached(); - - /** Called when no output sink was selected. Shows the GStreamer engine settings dialog. */ - void ErrorNoOutput(); - - /** Transmits new decoded metadata to the application */ - void NewMetaData(); + void HandlePipelineError(const QString& message); + void NewMetaData(const Engine::SimpleMetaBundle& bundle); + void NewBuffer(GstBuffer* buf); + void ClearScopeQ(); private: - static GstEngine* instance() { return sInstance; } - - /** - * Creates a GStreamer element and puts it into pipeline. - * @param factoryName Name of the element class to create. - * @param bin Container into which the element is put. - * @param name Identifier for the element. - * @return Pointer to the created element, or NULL for failure. - */ - static GstElement* CreateElement( - const QString& factoryName, GstElement* bin = 0, const QString& name = 0); - - // CALLBACKS: - /** Bus message */ - static GstBusSyncReply BusCallbackSync( GstBus*, GstMessage*, gpointer ); - static gboolean BusCallback(GstBus*, GstMessage*, gpointer); - /** Called when decodebin has generated a new pad */ - static void NewPadCallback(GstElement*, GstPad*, gboolean, gpointer); - /** Used by canDecode(). When called, the format probably can be decoded */ + // Callbacks static void CanDecodeNewPadCallback(GstElement*, GstPad*, gboolean, gpointer); - /** Used by canDecode(). Called after last pad so it makes no sense to wait anymore */ static void CanDecodeLastCallback(GstElement*, gpointer); - /** Called when new metadata tags have been found */ - static void EventCallback( GstPad*, GstEvent* event, gpointer arg); - /** Duplicates audio data for application side processing */ - static void HandoffCallback( GstPad*, GstBuffer*, gpointer ); /** Get a list of available plugins from a specified Class */ PluginDetailsList GetPluginList(const QString& classname) const; /** Construct the output pipeline */ - bool CreatePipeline(); - - /** Stops playback, destroys all input pipelines, destroys output pipeline, and frees resources */ - void DestroyPipeline(); - - /* Constructs the pipeline for audio CDs, optionally selecting a device and/or track and/or setting the state to paused */ - bool SetupAudioCD( const QString& device, unsigned track, bool pause ); + boost::shared_ptr CreatePipeline(const QUrl& url); /** Beams the streaming buffer status to Amarok */ void SendBufferStatus(); @@ -156,33 +123,11 @@ class GstEngine : public Engine::Base { ///////////////////////////////////////////////////////////////////////////////////// // Interval of main timer, handles the volume fading static const int kTimerInterval = 40; //msec - static const int kGstStateTimeout = 10000000; QString sink_; QString device_; - static GstEngine* sInstance; - - GstElement* gst_pipeline_; - - GstElement* gst_src_; - GstElement* gst_decodebin_; - - GstElement* gst_audiobin_; - - GstElement* gst_audioconvert_; - GstElement* gst_equalizer_; - GstElement* gst_identity_; - GstElement* gst_volume_; - GstElement* gst_audioscale_; - GstElement* gst_audiosink_; - - QString gst_error_; - QString gst_debug_; - - int metacount_; - - uint event_cb_id_; + boost::shared_ptr current_pipeline_; ////////// // scope @@ -197,19 +142,15 @@ class GstEngine : public Engine::Base { void UpdateScope(); qint64 PruneScope(); - void ClearScopeQ(); QMutex scope_mutex_; - bool pipeline_filled_; float fade_value_; bool equalizer_enabled_; int equalizer_preamp_; QList equalizer_gains_; - Engine::SimpleMetaBundle meta_bundle_; - bool shutdown_; mutable bool can_decode_success_; mutable bool can_decode_last_; diff --git a/src/engines/gstenginepipeline.cpp b/src/engines/gstenginepipeline.cpp new file mode 100644 index 000000000..d642f6015 --- /dev/null +++ b/src/engines/gstenginepipeline.cpp @@ -0,0 +1,299 @@ +/* This file is part of Clementine. + + Clementine 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. + + Clementine 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 Clementine. If not, see . +*/ + +#include "gstenginepipeline.h" +#include "gstequalizer.h" +#include "gstengine.h" + +#include + +GstEnginePipeline::GstEnginePipeline() + : QObject(NULL), + valid_(false), + sink_(GstEngine::kAutoSink), + pipeline_(NULL), + src_(NULL), + decodebin_(NULL), + audiobin_(NULL), + audioconvert_(NULL), + equalizer_(NULL), + identity_(NULL), + volume_(NULL), + audioscale_(NULL), + audiosink_(NULL), + event_cb_id_(0) +{ +} + +void GstEnginePipeline::set_output_device(const QString &sink, const QString &device) { + sink_ = sink; + device_ = device; +} + +bool GstEnginePipeline::Init(const QUrl &url) { + pipeline_ = gst_pipeline_new("pipeline"); + + // Source + src_ = GstEngine::CreateElement("giosrc"); + if (!src_) + return false; + + g_object_set(G_OBJECT(src_), "location", url.toEncoded().constData(), NULL); + gst_bin_add(GST_BIN(pipeline_), src_); + + // Decode bin + if (!(decodebin_ = GstEngine::CreateElement("decodebin", pipeline_))) { return false; } + g_signal_connect(G_OBJECT(decodebin_), "new-decoded-pad", G_CALLBACK(NewPadCallback), this); + + GstPad* pad = gst_element_get_pad(decodebin_, "sink"); + if (pad) { + event_cb_id_ = gst_pad_add_event_probe (pad, G_CALLBACK(EventCallback), this); + gst_object_unref(pad); + } + + // The link from decodebin to audioconvert will be made in the newPad-callback + gst_element_link(src_, decodebin_); + + // Audio bin + audiobin_ = gst_bin_new("audiobin"); + gst_bin_add(GST_BIN(pipeline_), audiobin_); + + if (!(audiosink_ = GstEngine::CreateElement(sink_, audiobin_))) + return false; + + if (GstEngine::DoesThisSinkSupportChangingTheOutputDeviceToAUserEditableString(sink_) && !device_.isEmpty()) + g_object_set(G_OBJECT(audiosink_), "device", device_.toUtf8().constData(), NULL); + + equalizer_ = GST_ELEMENT(gst_equalizer_new()); + gst_bin_add(GST_BIN(audiobin_), equalizer_); + + if (!(audioconvert_ = GstEngine::CreateElement("audioconvert", audiobin_))) { return false; } + if (!(identity_ = GstEngine::CreateElement("identity", audiobin_))) { return false; } + if (!(volume_ = GstEngine::CreateElement("volume", audiobin_))) { return false; } + if (!(audioscale_ = GstEngine::CreateElement("audioresample", audiobin_))) { return false; } + + pad = gst_element_get_pad(audioconvert_, "sink"); + gst_element_add_pad(audiobin_, gst_ghost_pad_new("sink", pad)); + gst_object_unref(pad); + + // add a data probe on the src pad if the audioconvert element for our scope + // we do it here because we want pre-equalized and pre-volume samples + // so that our visualization are not affected by them + pad = gst_element_get_pad(audioconvert_, "src"); + gst_pad_add_buffer_probe(pad, G_CALLBACK(HandoffCallback), this); + gst_object_unref (pad); + + // Ensure we get the right type out of audioconvert for our scope + GstCaps* caps = gst_caps_new_simple ("audio/x-raw-int", + "width", G_TYPE_INT, 16, + "signed", G_TYPE_BOOLEAN, true, + NULL); + gst_element_link_filtered(audioconvert_, equalizer_, caps); + gst_caps_unref(caps); + + // Add an extra audioconvert at the end as osxaudiosink supports only one format. + GstElement* convert = GstEngine::CreateElement("audioconvert", audiobin_, "FFFUUUU"); + if (!convert) { return false; } + gst_element_link_many(equalizer_, identity_, volume_, + audioscale_, convert, audiosink_, NULL); + + gst_bus_set_sync_handler(gst_pipeline_get_bus(GST_PIPELINE(pipeline_)), BusCallbackSync, this); + gst_bus_add_watch(gst_pipeline_get_bus(GST_PIPELINE(pipeline_)), BusCallback, this); + + return true; +} + +GstEnginePipeline::~GstEnginePipeline() { + // We don't want an EOS signal from the decodebin + if (decodebin_) { + GstPad *p = gst_element_get_pad(decodebin_, "sink"); + if (p) + gst_pad_remove_event_probe(p, event_cb_id_); + } + + if (pipeline_) { + gst_element_set_state(pipeline_, GST_STATE_NULL); + gst_object_unref(GST_OBJECT(pipeline_)); + } +} + + + +gboolean GstEnginePipeline::BusCallback(GstBus*, GstMessage* msg, gpointer self) { + GstEnginePipeline* instance = static_cast(self); + + switch ( GST_MESSAGE_TYPE(msg)) { + case GST_MESSAGE_ERROR: { + GError* error; + gchar* debugs; + + gst_message_parse_error(msg, &error, &debugs); + qDebug() << "ERROR RECEIVED IN BUS_CB <" << error->message << ">" ; + + emit instance->Error(QString::fromAscii(error->message)); + break; + } + + case GST_MESSAGE_TAG: { + gchar* string=NULL; + Engine::SimpleMetaBundle bundle; + GstTagList* taglist; + gst_message_parse_tag(msg,&taglist); + bool success = false; + + if ( gst_tag_list_get_string( taglist, GST_TAG_TITLE, &string ) && string ) { + qDebug() << "received tag 'Title': " << QString( string ) ; + bundle.title = string; + success = true; + } + if ( gst_tag_list_get_string( taglist, GST_TAG_ARTIST, &string ) && string ) { + qDebug() << "received tag 'Artist': " << QString( string ) ; + bundle.artist = string; + success = true; + } + if ( gst_tag_list_get_string( taglist, GST_TAG_COMMENT, &string ) && string ) { + qDebug() << "received tag 'Comment': " << QString( string ) ; + bundle.comment = string; + success = true; + } + if ( gst_tag_list_get_string( taglist, GST_TAG_ALBUM, &string ) && string ) { + qDebug() << "received tag 'Album': " << QString( string ) ; + bundle.album = string; + success = true; + } + g_free(string); + gst_tag_list_free(taglist); + if (success) + emit instance->MetadataFound(bundle); + break; + } + + default: + break; + } + return GST_BUS_DROP; +} + +GstBusSyncReply GstEnginePipeline::BusCallbackSync(GstBus*, GstMessage* msg, gpointer self) { + GstEnginePipeline* instance = static_cast(self); + switch (GST_MESSAGE_TYPE(msg)) { + case GST_MESSAGE_EOS: + emit instance->EndOfStreamReached(); + break; + + default: + break; + } + + return GST_BUS_PASS; +} + + +void GstEnginePipeline::NewPadCallback(GstElement*, GstPad* pad, gboolean, gpointer self) { + GstEnginePipeline* instance = static_cast(self); + GstPad* const audiopad = gst_element_get_pad(instance->audiobin_, "sink"); + + if (GST_PAD_IS_LINKED(audiopad)) { + qDebug() << "audiopad is already linked. Unlinking old pad." ; + gst_pad_unlink(audiopad, GST_PAD_PEER(audiopad)); + } + + gst_pad_link(pad, audiopad); + + gst_object_unref(audiopad); +} + + +void GstEnginePipeline::HandoffCallback(GstPad*, GstBuffer* buf, gpointer self) { + GstEnginePipeline* instance = static_cast(self); + + if (instance->forwards_buffers_) { + gst_buffer_ref(buf); + emit instance->BufferFound(buf); + } +} + +void GstEnginePipeline::EventCallback(GstPad*, GstEvent* event, gpointer self) { + GstEnginePipeline* instance = static_cast(self); + + switch(static_cast(event->type)) { + case GST_EVENT_EOS: + emit instance->EndOfStreamReached(); + break; + + default: + break; + } +} + +qint64 GstEnginePipeline::position() const { + GstFormat fmt = GST_FORMAT_TIME; + gint64 value = 0; + gst_element_query_position(pipeline_, &fmt, &value); + + return value; +} + + +qint64 GstEnginePipeline::length() const { + GstFormat fmt = GST_FORMAT_TIME; + gint64 value = 0; + gst_element_query_duration(pipeline_, &fmt, &value); + + return value; +} + + +GstState GstEnginePipeline::state() const { + GstState s, sp; + if (gst_element_get_state(pipeline_, &s, &sp, kGstStateTimeout) == + GST_STATE_CHANGE_FAILURE) + return GST_STATE_NULL; + + return s; +} + +bool GstEnginePipeline::SetState(GstState state) { + return gst_element_set_state(pipeline_, state) != GST_STATE_CHANGE_FAILURE; +} + +bool GstEnginePipeline::Seek(qint64 nanosec) { + return gst_element_seek(pipeline_, 1.0, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, + GST_SEEK_TYPE_SET, nanosec, GST_SEEK_TYPE_NONE, + GST_CLOCK_TIME_NONE); +} + +void GstEnginePipeline::SetEqualizerEnabled(bool enabled) { + g_object_set(G_OBJECT(equalizer_), "active", enabled, NULL); +} + + +void GstEnginePipeline::SetEqualizerParams(int preamp, const QList& band_gains) { + // Preamp + g_object_set(G_OBJECT(equalizer_), "preamp", ( preamp + 100 ) / 2, NULL); + + // Gains + std::vector gains_temp; + gains_temp.resize( band_gains.count() ); + for ( int i = 0; i < band_gains.count(); i++ ) + gains_temp[i] = band_gains.at( i ) + 100; + + g_object_set(G_OBJECT(equalizer_), "gain", &gains_temp, NULL); +} + +void GstEnginePipeline::SetVolume(int percent) { + g_object_set(G_OBJECT(volume_), "volume", double(percent) * 0.01, NULL); +} diff --git a/src/engines/gstenginepipeline.h b/src/engines/gstenginepipeline.h new file mode 100644 index 000000000..69f495b02 --- /dev/null +++ b/src/engines/gstenginepipeline.h @@ -0,0 +1,99 @@ +/* This file is part of Clementine. + + Clementine 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. + + Clementine 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 Clementine. If not, see . +*/ + +#ifndef GSTENGINEPIPELINE_H +#define GSTENGINEPIPELINE_H + +#include +#include + +#include + +#include "engine_fwd.h" + +class GstEngine; + +class GstEnginePipeline : public QObject { + Q_OBJECT + + public: + GstEnginePipeline(); + ~GstEnginePipeline(); + + // Call these setters before Init + void set_output_device(const QString& sink, const QString& device); + void set_forwards_buffers(bool v) { forwards_buffers_ = v; } + + // Creates the pipeline, returns false on error + bool Init(const QUrl& url); + + // Control the music playback + bool SetState(GstState state); + bool Seek(qint64 nanosec); + void SetEqualizerEnabled(bool enabled); + void SetEqualizerParams(int preamp, const QList& band_gains); + void SetVolume(int percent); + + // Get information about the music playback + bool is_valid() const { return valid_; } + qint64 position() const; + qint64 length() const; + GstState state() const; + + signals: + void EndOfStreamReached(); + void MetadataFound(const Engine::SimpleMetaBundle& bundle); + void BufferFound(GstBuffer* buf); + void Error(const QString& message); + + private: + // Static callbacks. The GstEnginePipeline instance is passed in the last + // argument. + static GstBusSyncReply BusCallbackSync(GstBus*, GstMessage*, gpointer); + static gboolean BusCallback(GstBus*, GstMessage*, gpointer); + static void NewPadCallback(GstElement*, GstPad*, gboolean, gpointer); + static void HandoffCallback(GstPad*, GstBuffer*, gpointer); + static void EventCallback(GstPad*, GstEvent*, gpointer); + + private: + static const int kGstStateTimeout = 10000000; + + bool valid_; + QString sink_; + QString device_; + bool forwards_buffers_; + + GstElement* pipeline_; + + // Bins + // src ! decodebin ! audiobin + GstElement* src_; + GstElement* decodebin_; + GstElement* audiobin_; + + // Elements in the audiobin + // audioconvert ! equalizer ! identity ! volume ! audioscale ! audioconvert ! audiosink + GstElement* audioconvert_; + GstElement* equalizer_; + GstElement* identity_; + GstElement* volume_; + GstElement* audioscale_; + GstElement* audiosink_; + + uint event_cb_id_; +}; + +#endif // GSTENGINEPIPELINE_H