/* This file is part of the KDE project. Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). This library is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 2.1 or 3 of the License. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #include #include #include "common.h" #include "mediaobject.h" #include "videowidget.h" #include "message.h" #include "backend.h" #include "streamreader.h" #include "phononsrc.h" #include #include #include #include #include #include #include #include #define ABOUT_TO_FINNISH_TIME 2000 #define MAX_QUEUE_TIME 20 * GST_SECOND QT_BEGIN_NAMESPACE namespace Phonon { namespace Gstreamer { MediaObject::MediaObject(Backend *backend, QObject *parent) : QObject(parent) , MediaNode(backend, AudioSource | VideoSource) , m_resumeState(false) , m_oldState(Phonon::LoadingState) , m_oldPos(0) , m_state(Phonon::LoadingState) , m_pendingState(Phonon::LoadingState) , m_tickTimer(new QTimer(this)) , m_prefinishMark(0) , m_transitionTime(0) , m_posAtSeek(-1) , m_prefinishMarkReachedNotEmitted(true) , m_aboutToFinishEmitted(false) , m_loading(false) , m_capsHandler(0) , m_datasource(0) , m_decodebin(0) , m_audioPipe(0) , m_videoPipe(0) , m_totalTime(-1) , m_bufferPercent(0) , m_hasVideo(false) , m_videoStreamFound(false) , m_hasAudio(false) , m_seekable(false) , m_atEndOfStream(false) , m_atStartOfStream(false) , m_error(Phonon::NoError) , m_pipeline(0) , m_audioGraph(0) , m_videoGraph(0) , m_previousTickTime(-1) , m_resetNeeded(false) , m_autoplayTitles(true) , m_availableTitles(0) , m_currentTitle(1) { qRegisterMetaType("GstCaps*"); qRegisterMetaType("State"); static int count = 0; m_name = "MediaObject" + QString::number(count++); if (!m_backend->isValid()) { setError(tr("Cannot start playback. \n\nCheck your Gstreamer installation and make sure you " "\nhave libgstreamer-plugins-base installed."), Phonon::FatalError); } else { m_root = this; createPipeline(); m_backend->addBusWatcher(this); connect(m_tickTimer, SIGNAL(timeout()), SLOT(emitTick())); } connect(this, SIGNAL(stateChanged(Phonon::State, Phonon::State)), this, SLOT(notifyStateChange(Phonon::State, Phonon::State))); } MediaObject::~MediaObject() { m_backend->removeBusWatcher(this); if (m_pipeline) { gst_element_set_state(m_pipeline, GST_STATE_NULL); gst_object_unref(m_pipeline); } if (m_audioGraph) { gst_element_set_state(m_audioGraph, GST_STATE_NULL); gst_object_unref(m_audioGraph); } if (m_videoGraph) { gst_element_set_state(m_videoGraph, GST_STATE_NULL); gst_object_unref(m_videoGraph); } } QString stateString(const Phonon::State &state) { switch (state) { case Phonon::LoadingState: return QString("LoadingState"); case Phonon::StoppedState: return QString("StoppedState"); case Phonon::PlayingState: return QString("PlayingState"); case Phonon::BufferingState: return QString("BufferingState"); case Phonon::PausedState: return QString("PausedState"); case Phonon::ErrorState: return QString("ErrorState"); } return QString(); } void MediaObject::saveState() { //Only first resumeState is respected if (m_resumeState) return; if (m_pendingState == Phonon::PlayingState || m_pendingState == Phonon::PausedState) { m_resumeState = true; m_oldState = m_pendingState; m_oldPos = getPipelinePos(); } } void MediaObject::resumeState() { if (m_resumeState) QMetaObject::invokeMethod(this, "setState", Qt::QueuedConnection, Q_ARG(State, m_oldState)); } void MediaObject::newPadAvailable (GstPad *pad) { GstCaps *caps; GstStructure *str; caps = gst_pad_get_caps (pad); if (caps) { str = gst_caps_get_structure (caps, 0); QString mediaString(gst_structure_get_name (str)); if (mediaString.startsWith("video")) { connectVideo(pad); } else if (mediaString.startsWith("audio")) { connectAudio(pad); } else { m_backend->logMessage("Could not connect pad", Backend::Warning); } gst_caps_unref (caps); } } void MediaObject::cb_newpad (GstElement *decodebin, GstPad *pad, gboolean last, gpointer data) { Q_UNUSED(decodebin); Q_UNUSED(pad); Q_UNUSED(last); Q_UNUSED(data); MediaObject *media = static_cast(data); Q_ASSERT(media); media->newPadAvailable(pad); } void MediaObject::noMorePadsAvailable () { if (m_missingCodecs.size() > 0) { bool canPlay = (m_hasAudio || m_videoStreamFound); Phonon::ErrorType error = canPlay ? Phonon::NormalError : Phonon::FatalError; if (error == Phonon::NormalError && m_hasVideo && !m_videoStreamFound) { m_hasVideo = false; emit hasVideoChanged(false); } QString codecs = m_missingCodecs.join(", "); setError(QString(tr("A required codec is missing. You need to install the following codec(s) to play this content: %0")).arg(codecs), error); m_missingCodecs.clear(); } } void MediaObject::cb_no_more_pads (GstElement * decodebin, gpointer data) { Q_UNUSED(decodebin); MediaObject *media = static_cast(data); Q_ASSERT(media); QMetaObject::invokeMethod(media, "noMorePadsAvailable", Qt::QueuedConnection); } typedef void (*Ptr_gst_pb_utils_init)(); typedef gchar* (*Ptr_gst_pb_utils_get_codec_description)(const GstCaps *); void MediaObject::cb_unknown_type (GstElement *decodebin, GstPad *pad, GstCaps *caps, gpointer data) { Q_UNUSED(decodebin); Q_UNUSED(pad); MediaObject *media = static_cast(data); Q_ASSERT(media); QString value = "unknown codec"; // These functions require GStreamer > 0.10.12 static Ptr_gst_pb_utils_init p_gst_pb_utils_init = 0; static Ptr_gst_pb_utils_get_codec_description p_gst_pb_utils_get_codec_description = 0; if (!p_gst_pb_utils_init) { p_gst_pb_utils_init = (Ptr_gst_pb_utils_init)QLibrary::resolve(QLatin1String("gstpbutils-0.10"), 0, "gst_pb_utils_init"); p_gst_pb_utils_get_codec_description = (Ptr_gst_pb_utils_get_codec_description)QLibrary::resolve(QLatin1String("gstpbutils-0.10"), 0, "gst_pb_utils_get_codec_description"); if (p_gst_pb_utils_init) p_gst_pb_utils_init(); } if (p_gst_pb_utils_get_codec_description) { gchar *codecName = NULL; codecName = p_gst_pb_utils_get_codec_description (caps); value = QString::fromUtf8(codecName); g_free (codecName); } else { // For GStreamer versions < 0.10.12 GstStructure *str = gst_caps_get_structure (caps, 0); value = QString::fromUtf8(gst_structure_get_name (str)); } media->addMissingCodecName(value); } static void notifyVideoCaps(GObject *obj, GParamSpec *, gpointer data) { GstPad *pad = GST_PAD(obj); GstCaps *caps = gst_pad_get_caps (pad); Q_ASSERT(caps); MediaObject *media = static_cast(data); // We do not want any more notifications until the source changes g_signal_handler_disconnect(pad, media->capsHandler()); // setVideoCaps calls loadingComplete(), meaning we cannot call it from // the streaming thread QMetaObject::invokeMethod(media, "setVideoCaps", Qt::QueuedConnection, Q_ARG(GstCaps *, caps)); } void MediaObject::setVideoCaps(GstCaps *caps) { GstStructure *str; gint width, height; if ((str = gst_caps_get_structure (caps, 0))) { if (gst_structure_get_int (str, "width", &width) && gst_structure_get_int (str, "height", &height)) { gint aspectNum = 0; gint aspectDenum = 0; if (gst_structure_get_fraction(str, "pixel-aspect-ratio", &aspectNum, &aspectDenum)) { if (aspectDenum > 0) width = width*aspectNum/aspectDenum; } // Let child nodes know about our new video size QSize size(width, height); MediaNodeEvent event(MediaNodeEvent::VideoSizeChanged, &size); notify(&event); } } gst_caps_unref(caps); } // Adds an element to the pipeline if not previously added bool MediaObject::addToPipeline(GstElement *elem) { bool success = true; if (!GST_ELEMENT_PARENT(elem)) { // If not already in pipeline success = gst_bin_add(GST_BIN(m_pipeline), elem); } return success; } void MediaObject::connectVideo(GstPad *pad) { GstState currentState = GST_STATE(m_pipeline); if (addToPipeline(m_videoGraph)) { GstPad *videopad = gst_element_get_pad (m_videoGraph, "sink"); if (!GST_PAD_IS_LINKED (videopad) && (gst_pad_link (pad, videopad) == GST_PAD_LINK_OK)) { gst_element_set_state(m_videoGraph, currentState == GST_STATE_PLAYING ? GST_STATE_PLAYING : GST_STATE_PAUSED); m_videoStreamFound = true; m_backend->logMessage("Video track connected", Backend::Info, this); // Note that the notify::caps _must_ be installed after linking to work with Dapper m_capsHandler = g_signal_connect(pad, "notify::caps", G_CALLBACK(notifyVideoCaps), this); if (!m_loading && !m_hasVideo) { m_hasVideo = m_videoStreamFound; emit hasVideoChanged(m_hasVideo); } } gst_object_unref (videopad); } else { m_backend->logMessage("The video stream could not be plugged.", Backend::Info, this); } } void MediaObject::connectAudio(GstPad *pad) { GstState currentState = GST_STATE(m_pipeline); if (addToPipeline(m_audioGraph)) { GstPad *audiopad = gst_element_get_pad (m_audioGraph, "sink"); if (!GST_PAD_IS_LINKED (audiopad) && (gst_pad_link (pad, audiopad)==GST_PAD_LINK_OK)) { gst_element_set_state(m_audioGraph, currentState == GST_STATE_PLAYING ? GST_STATE_PLAYING : GST_STATE_PAUSED); m_hasAudio = true; m_backend->logMessage("Audio track connected", Backend::Info, this); } gst_object_unref (audiopad); } else { m_backend->logMessage("The audio stream could not be plugged.", Backend::Info, this); } } void MediaObject::cb_pad_added(GstElement *decodebin, GstPad *pad, gpointer data) { Q_UNUSED(decodebin); GstPad *decodepad = static_cast(data); gst_pad_link (pad, decodepad); gst_object_unref (decodepad); } /** * Create a media source from a given URL. * * returns true if successful */ bool MediaObject::createPipefromURL(const QUrl &url) { // Remove any existing data source if (m_datasource) { gst_bin_remove(GST_BIN(m_pipeline), m_datasource); // m_pipeline has the only ref to datasource m_datasource = 0; } // Verify that the uri can be parsed if (!url.isValid()) { m_backend->logMessage(QString("%1 is not a valid URI").arg(url.toString())); return false; } // Create a new datasource based on the input URL QByteArray encoded_cstr_url = url.toEncoded(); m_datasource = gst_element_make_from_uri(GST_URI_SRC, encoded_cstr_url.constData(), (const char*)NULL); if (!m_datasource) return false; // Link data source into pipeline gst_bin_add(GST_BIN(m_pipeline), m_datasource); if (!gst_element_link(m_datasource, m_decodebin)) { // For sources with dynamic pads (such as RtspSrc) we need to connect dynamically GstPad *decodepad = gst_element_get_pad (m_decodebin, "sink"); g_signal_connect (m_datasource, "pad-added", G_CALLBACK (&cb_pad_added), decodepad); } return true; } /** * Create a media source from a media stream * * returns true if successful */ bool MediaObject::createPipefromStream(const MediaSource &source) { // Remove any existing data source if (m_datasource) { gst_bin_remove(GST_BIN(m_pipeline), m_datasource); // m_pipeline has the only ref to datasource m_datasource = 0; } m_datasource = GST_ELEMENT(g_object_new(phonon_src_get_type(), NULL)); if (!m_datasource) return false; StreamReader *streamReader = new StreamReader(source); g_object_set (G_OBJECT (m_datasource), "iodevice", streamReader, (const char*)NULL); // Link data source into pipeline gst_bin_add(GST_BIN(m_pipeline), m_datasource); if (!gst_element_link(m_datasource, m_decodebin)) { gst_bin_remove(GST_BIN(m_pipeline), m_datasource); return false; } return true; } void MediaObject::createPipeline() { m_pipeline = gst_pipeline_new (NULL); gst_object_ref (GST_OBJECT (m_pipeline)); gst_object_sink (GST_OBJECT (m_pipeline)); m_decodebin = gst_element_factory_make ("decodebin", NULL); g_signal_connect (m_decodebin, "new-decoded-pad", G_CALLBACK (&cb_newpad), this); g_signal_connect (m_decodebin, "unknown-type", G_CALLBACK (&cb_unknown_type), this); g_signal_connect (m_decodebin, "no-more-pads", G_CALLBACK (&cb_no_more_pads), this); gst_bin_add(GST_BIN(m_pipeline), m_decodebin); // Create a bin to contain the gst elements for this medianode // Set up audio graph m_audioGraph = gst_bin_new(NULL); gst_object_ref (GST_OBJECT (m_audioGraph)); gst_object_sink (GST_OBJECT (m_audioGraph)); // Note that these queues are only required for streaming content // And should ideally be created on demand as they will disable // pull-mode access. Also note that the max-size-time are increased to // reduce buffer overruns as these are not gracefully handled at the moment. m_audioPipe = gst_element_factory_make("queue", NULL); g_object_set(G_OBJECT(m_audioPipe), "max-size-time", MAX_QUEUE_TIME, (const char*)NULL); gst_bin_add(GST_BIN(m_audioGraph), m_audioPipe); GstPad *audiopad = gst_element_get_pad (m_audioPipe, "sink"); gst_element_add_pad (m_audioGraph, gst_ghost_pad_new ("sink", audiopad)); gst_object_unref (audiopad); // Set up video graph m_videoGraph = gst_bin_new(NULL); gst_object_ref (GST_OBJECT (m_videoGraph)); gst_object_sink (GST_OBJECT (m_videoGraph)); m_videoPipe = gst_element_factory_make("queue", NULL); g_object_set(G_OBJECT(m_videoPipe), "max-size-time", MAX_QUEUE_TIME, (const char*)NULL); gst_bin_add(GST_BIN(m_videoGraph), m_videoPipe); GstPad *videopad = gst_element_get_pad (m_videoPipe, "sink"); gst_element_add_pad (m_videoGraph, gst_ghost_pad_new ("sink", videopad)); gst_object_unref (videopad); if (m_pipeline && m_decodebin && m_audioGraph && m_videoGraph && m_audioPipe && m_videoPipe) m_isValid = true; else m_backend->logMessage("Could not create pipeline for media object", Backend::Warning); } /** * !reimp */ State MediaObject::state() const { return m_state; } /** * !reimp */ bool MediaObject::hasVideo() const { return m_hasVideo; } /** * !reimp */ bool MediaObject::isSeekable() const { return m_seekable; } /** * !reimp */ qint64 MediaObject::currentTime() const { if (m_resumeState) return m_oldPos; switch (state()) { case Phonon::PausedState: case Phonon::BufferingState: case Phonon::PlayingState: return getPipelinePos(); case Phonon::StoppedState: case Phonon::LoadingState: return 0; case Phonon::ErrorState: break; } return -1; } /** * !reimp */ qint32 MediaObject::tickInterval() const { return m_tickInterval; } /** * !reimp */ void MediaObject::setTickInterval(qint32 newTickInterval) { m_tickInterval = newTickInterval; if (m_tickInterval <= 0) m_tickTimer->setInterval(50); else m_tickTimer->setInterval(newTickInterval); } /** * !reimp */ void MediaObject::play() { setState(Phonon::PlayingState); m_resumeState = false; } /** * !reimp */ QString MediaObject::errorString() const { return m_errorString; } /** * !reimp */ Phonon::ErrorType MediaObject::errorType() const { return m_error; } /** * Set the current state of the mediaObject. * * !### Note that both Playing and Paused states are set immediately * This should obviously be done in response to actual gstreamer state changes */ void MediaObject::setState(State newstate) { if (!isValid()) return; if (m_state == newstate) return; if (m_loading) { // We are still loading. The state will be requested // when loading has completed. m_pendingState = newstate; return; } GstState currentState; gst_element_get_state (m_pipeline, ¤tState, NULL, 1000); switch (newstate) { case Phonon::BufferingState: m_backend->logMessage("phonon state request: buffering", Backend::Info, this); break; case Phonon::PausedState: m_backend->logMessage("phonon state request: paused", Backend::Info, this); if (currentState == GST_STATE_PAUSED) { changeState(Phonon::PausedState); } else if (gst_element_set_state(m_pipeline, GST_STATE_PAUSED) != GST_STATE_CHANGE_FAILURE) { m_pendingState = Phonon::PausedState; } else { m_backend->logMessage("phonon state request failed", Backend::Info, this); } break; case Phonon::StoppedState: m_backend->logMessage("phonon state request: Stopped", Backend::Info, this); if (currentState == GST_STATE_READY) { changeState(Phonon::StoppedState); } else if (gst_element_set_state(m_pipeline, GST_STATE_READY) != GST_STATE_CHANGE_FAILURE) { m_pendingState = Phonon::StoppedState; } else { m_backend->logMessage("phonon state request failed", Backend::Info, this); } m_atEndOfStream = false; break; case Phonon::PlayingState: if (m_resetNeeded) { // ### Note this is a workaround and it should really be gracefully // handled by medianode when we implement live connections. // This generally happens if medianodes have been connected after the MediaSource was set // Note that a side-effect of this is that we resend all meta data. gst_element_set_state(m_pipeline, GST_STATE_NULL); m_resetNeeded = false; // Send a source change so the X11 renderer // will re-set the overlay MediaNodeEvent event(MediaNodeEvent::SourceChanged); notify(&event); } m_backend->logMessage("phonon state request: Playing", Backend::Info, this); if (m_atEndOfStream) { m_backend->logMessage("EOS already reached", Backend::Info, this); } else if (currentState == GST_STATE_PLAYING) { changeState(Phonon::PlayingState); } else if (!m_atEndOfStream && gst_element_set_state(m_pipeline, GST_STATE_PLAYING) != GST_STATE_CHANGE_FAILURE) { m_pendingState = Phonon::PlayingState; } else { m_backend->logMessage("phonon state request failed", Backend::Info, this); } break; case Phonon::ErrorState: m_backend->logMessage("phonon state request : Error", Backend::Warning, this); m_backend->logMessage(QString("Last error : %0").arg(errorString()) , Backend::Warning, this); changeState(Phonon::ErrorState); //immediately set error state break; case Phonon::LoadingState: m_backend->logMessage("phonon state request: Loading", Backend::Info, this); changeState(Phonon::LoadingState); break; } } /* * Signals that the requested state has completed * by emitting stateChanged and updates the internal state. */ void MediaObject::changeState(State newstate) { if (newstate == m_state) return; Phonon::State oldState = m_state; m_state = newstate; // m_state must be set before emitting, since // Error state requires that state() will return the new value m_pendingState = newstate; emit stateChanged(newstate, oldState); switch (newstate) { case Phonon::PausedState: m_backend->logMessage("phonon state changed: paused", Backend::Info, this); break; case Phonon::BufferingState: m_backend->logMessage("phonon state changed: buffering", Backend::Info, this); break; case Phonon::PlayingState: m_backend->logMessage("phonon state changed: Playing", Backend::Info, this); break; case Phonon::StoppedState: m_backend->logMessage("phonon state changed: Stopped", Backend::Info, this); m_tickTimer->stop(); break; case Phonon::ErrorState: m_loading = false; m_backend->logMessage("phonon state changed : Error", Backend::Info, this); m_backend->logMessage(errorString(), Backend::Warning, this); break; case Phonon::LoadingState: m_backend->logMessage("phonon state changed: Loading", Backend::Info, this); break; } } void MediaObject::setError(const QString &errorString, Phonon::ErrorType error) { m_errorString = errorString; m_error = error; m_tickTimer->stop(); if (error == Phonon::FatalError) { m_hasVideo = false; emit hasVideoChanged(false); gst_element_set_state(m_pipeline, GST_STATE_READY); changeState(Phonon::ErrorState); } else { if (m_loading) //Flag error only after loading has completed m_pendingState = Phonon::ErrorState; else changeState(Phonon::ErrorState); } } qint64 MediaObject::totalTime() const { return m_totalTime; } qint32 MediaObject::prefinishMark() const { return m_prefinishMark; } qint32 MediaObject::transitionTime() const { return m_transitionTime; } void MediaObject::setTransitionTime(qint32 time) { m_transitionTime = time; } qint64 MediaObject::remainingTime() const { return totalTime() - currentTime(); } MediaSource MediaObject::source() const { return m_source; } void MediaObject::setNextSource(const MediaSource &source) { if (source.type() == MediaSource::Invalid && source.type() == MediaSource::Empty) return; m_nextSource = source; } /** * Update total time value from the pipeline */ bool MediaObject::updateTotalTime() { GstFormat format = GST_FORMAT_TIME; gint64 duration = 0; if (gst_element_query_duration (GST_ELEMENT(m_pipeline), &format, &duration)) { setTotalTime(duration / GST_MSECOND); return true; } return false; } /** * Checks if the current source is seekable */ void MediaObject::updateSeekable() { if (!isValid()) return; GstQuery *query; gboolean result; gint64 start, stop; query = gst_query_new_seeking(GST_FORMAT_TIME); result = gst_element_query (m_pipeline, query); if (result) { gboolean seekable; GstFormat format; gst_query_parse_seeking (query, &format, &seekable, &start, &stop); if (m_seekable != seekable) { m_seekable = seekable; emit seekableChanged(m_seekable); } if (m_seekable) m_backend->logMessage("Stream is seekable", Backend::Info, this); else m_backend->logMessage("Stream is non-seekable", Backend::Info, this); } else { m_backend->logMessage("updateSeekable query failed", Backend::Info, this); } gst_query_unref (query); } qint64 MediaObject::getPipelinePos() const { Q_ASSERT(m_pipeline); // Note some formats (usually mpeg) do not allow us to accurately seek to the // beginning or end of the file so we 'fake' it here rather than exposing the front end to potential issues. if (m_atEndOfStream) return totalTime(); if (m_atStartOfStream) return 0; if (m_posAtSeek >= 0) return m_posAtSeek; gint64 pos = 0; GstFormat format = GST_FORMAT_TIME; gst_element_query_position (GST_ELEMENT(m_pipeline), &format, &pos); return (pos / GST_MSECOND); } /* * Internal method to set a new total time for the media object */ void MediaObject::setTotalTime(qint64 newTime) { if (newTime == m_totalTime) return; m_totalTime = newTime; emit totalTimeChanged(m_totalTime); } /* * !reimp */ void MediaObject::setSource(const MediaSource &source) { if (!isValid()) return; // We have to reset the state completely here, otherwise // remnants of the old pipeline can result in strangenes // such as failing duration queries etc GstState state; gst_element_set_state(m_pipeline, GST_STATE_NULL); gst_element_get_state (m_pipeline, &state, NULL, 2000); m_source = source; emit currentSourceChanged(m_source); m_previousTickTime = -1; m_missingCodecs.clear(); // Go into to loading state changeState(Phonon::LoadingState); m_loading = true; m_resetNeeded = false; m_resumeState = false; m_pendingState = Phonon::StoppedState; // Make sure we start out unconnected if (GST_ELEMENT_PARENT(m_audioGraph)) gst_bin_remove(GST_BIN(m_pipeline), m_audioGraph); if (GST_ELEMENT_PARENT(m_videoGraph)) gst_bin_remove(GST_BIN(m_pipeline), m_videoGraph); // Clear any existing errors m_aboutToFinishEmitted = false; m_error = NoError; m_errorString = QString(); m_bufferPercent = 0; m_prefinishMarkReachedNotEmitted = true; m_aboutToFinishEmitted = false; m_hasAudio = false; m_videoStreamFound = false; setTotalTime(-1); m_atEndOfStream = false; // Clear exising meta tags m_metaData.clear(); switch (source.type()) { case MediaSource::Url: { if (createPipefromURL(source.url())) m_loading = true; else setError(tr("Could not open media source.")); } break; case MediaSource::LocalFile: { if (createPipefromURL(QUrl::fromLocalFile(source.fileName()))) m_loading = true; else setError(tr("Could not open media source.")); } break; case MediaSource::Invalid: setError(tr("Invalid source type."), Phonon::NormalError); break; case MediaSource::Empty: break; case MediaSource::Stream: if (createPipefromStream(source)) m_loading = true; else setError(tr("Could not open media source.")); break; case MediaSource::Disc: // CD tracks can be specified by setting the url in the following way uri=cdda:4 { QUrl cdurl(QLatin1String("cdda://")); if (createPipefromURL(cdurl)) m_loading = true; else setError(tr("Could not open media source.")); } break; default: m_backend->logMessage("Source type not currently supported", Backend::Warning, this); setError(tr("Could not open media source."), Phonon::NormalError); break; } MediaNodeEvent event(MediaNodeEvent::SourceChanged); notify(&event); // We need to link this node to ensure that fake sinks are connected // before loading, otherwise the stream will be blocked if (m_loading) link(); beginLoad(); } void MediaObject::beginLoad() { if (gst_element_set_state(m_pipeline, GST_STATE_PAUSED) != GST_STATE_CHANGE_FAILURE) { m_backend->logMessage("Begin source load", Backend::Info, this); } else { setError(tr("Could not open media source.")); } } // Called when we are ready to leave the loading state void MediaObject::loadingComplete() { if (m_videoStreamFound) { MediaNodeEvent event(MediaNodeEvent::VideoAvailable); notify(&event); } getStreamInfo(); m_loading = false; setState(m_pendingState); emit metaDataChanged(m_metaData); } void MediaObject::getStreamInfo() { updateSeekable(); updateTotalTime(); if (m_videoStreamFound != m_hasVideo) { m_hasVideo = m_videoStreamFound; emit hasVideoChanged(m_hasVideo); } m_availableTitles = 1; gint64 titleCount; GstFormat format = gst_format_get_by_nick("track"); if (gst_element_query_duration (m_pipeline, &format, &titleCount)) { //check if returned format is still "track", //gstreamer sometimes returns the total time, if tracks information is not available. if (qstrcmp(gst_format_get_name(format), "track") == 0) { int oldAvailableTitles = m_availableTitles; m_availableTitles = (int)titleCount; if (m_availableTitles != oldAvailableTitles) { emit availableTitlesChanged(m_availableTitles); m_backend->logMessage(QString("Available titles changed: %0").arg(m_availableTitles), Backend::Info, this); } } } } void MediaObject::setPrefinishMark(qint32 newPrefinishMark) { m_prefinishMark = newPrefinishMark; if (currentTime() < totalTime() - m_prefinishMark) // not about to finish m_prefinishMarkReachedNotEmitted = true; } void MediaObject::pause() { m_backend->logMessage("pause()", Backend::Info, this); if (state() != Phonon::PausedState) setState(Phonon::PausedState); m_resumeState = false; } void MediaObject::stop() { if (state() != Phonon::StoppedState) { setState(Phonon::StoppedState); m_prefinishMarkReachedNotEmitted = true; } m_resumeState = false; } void MediaObject::seek(qint64 time) { if (!isValid()) return; if (isSeekable()) { switch (state()) { case Phonon::PlayingState: case Phonon::StoppedState: case Phonon::PausedState: case Phonon::BufferingState: m_backend->logMessage(QString("Seek to pos %0").arg(time), Backend::Info, this); if (time <= 0) m_atStartOfStream = true; else m_atStartOfStream = false; m_posAtSeek = getPipelinePos(); m_tickTimer->stop(); if (gst_element_seek(m_pipeline, 1.0, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, GST_SEEK_TYPE_SET, time * GST_MSECOND, GST_SEEK_TYPE_NONE, GST_CLOCK_TIME_NONE)) break; case Phonon::LoadingState: case Phonon::ErrorState: return; } quint64 current = currentTime(); quint64 total = totalTime(); if (current < total - m_prefinishMark) m_prefinishMarkReachedNotEmitted = true; if (current < total - ABOUT_TO_FINNISH_TIME) m_aboutToFinishEmitted = false; m_atEndOfStream = false; } } void MediaObject::emitTick() { if (m_resumeState) { return; } qint64 currentTime = getPipelinePos(); qint64 totalTime = m_totalTime; if (m_tickInterval > 0 && currentTime != m_previousTickTime) { emit tick(currentTime); m_previousTickTime = currentTime; } if (m_state == Phonon::PlayingState) { if (currentTime >= totalTime - m_prefinishMark) { if (m_prefinishMarkReachedNotEmitted) { m_prefinishMarkReachedNotEmitted = false; emit prefinishMarkReached(totalTime - currentTime); } } // Prepare load of next source if (currentTime >= totalTime - ABOUT_TO_FINNISH_TIME) { if (!m_aboutToFinishEmitted) { m_aboutToFinishEmitted = true; // track is about to finish emit aboutToFinish(); } } } } /* * Used to iterate through the gst_tag_list and extract values */ void foreach_tag_function(const GstTagList *list, const gchar *tag, gpointer user_data) { TagMap *newData = static_cast(user_data); QString value; GType type = gst_tag_get_type(tag); switch (type) { case G_TYPE_STRING: { char *str = 0; gst_tag_list_get_string(list, tag, &str); value = QString::fromUtf8(str); g_free(str); } break; case G_TYPE_BOOLEAN: { int bval; gst_tag_list_get_boolean(list, tag, &bval); value = QString::number(bval); } break; case G_TYPE_INT: { int ival; gst_tag_list_get_int(list, tag, &ival); value = QString::number(ival); } break; case G_TYPE_UINT: { unsigned int uival; gst_tag_list_get_uint(list, tag, &uival); value = QString::number(uival); } break; case G_TYPE_FLOAT: { float fval; gst_tag_list_get_float(list, tag, &fval); value = QString::number(fval); } break; case G_TYPE_DOUBLE: { double dval; gst_tag_list_get_double(list, tag, &dval); value = QString::number(dval); } break; default: //qDebug("Unsupported tag type: %s", g_type_name(type)); break; } QString key = QString(tag).toUpper(); QString currVal = newData->value(key); if (!value.isEmpty() && !(newData->contains(key) && currVal == value)) newData->insert(key, value); } /** * Triggers playback after a song has completed in the current media queue */ void MediaObject::beginPlay() { setSource(m_nextSource); m_nextSource = MediaSource(); m_pendingState = Phonon::PlayingState; } /** * Handle GStreamer bus messages */ void MediaObject::handleBusMessage(const Message &message) { if (!isValid()) return; GstMessage *gstMessage = message.rawMessage(); Q_ASSERT(m_pipeline); if (m_backend->debugLevel() >= Backend::Debug) { int type = GST_MESSAGE_TYPE(gstMessage); gchar* name = gst_element_get_name(gstMessage->src); QString msgString = QString("Bus: %0 (%1)").arg(gst_message_type_get_name ((GstMessageType)type)).arg(name); g_free(name); m_backend->logMessage(msgString, Backend::Debug, this); } switch (GST_MESSAGE_TYPE (gstMessage)) { case GST_MESSAGE_EOS: m_backend->logMessage("EOS recieved", Backend::Info, this); handleEndOfStream(); break; case GST_MESSAGE_TAG: { GstTagList* tag_list = 0; gst_message_parse_tag(gstMessage, &tag_list); if (tag_list) { TagMap oldMap = m_metaData; // Keep a copy of the old one for reference // Append any new meta tags to the existing tag list gst_tag_list_foreach (tag_list, &foreach_tag_function, &m_metaData); m_backend->logMessage("Meta tags found", Backend::Info, this); if (oldMap != m_metaData && !m_loading) emit metaDataChanged(m_metaData); gst_tag_list_free(tag_list); } } break; case GST_MESSAGE_STATE_CHANGED : { if (gstMessage->src != GST_OBJECT(m_pipeline)) return; GstState oldState; GstState newState; GstState pendingState; gst_message_parse_state_changed (gstMessage, &oldState, &newState, &pendingState); if (newState == pendingState) return; m_posAtSeek = -1; switch (newState) { case GST_STATE_PLAYING : m_atStartOfStream = false; m_backend->logMessage("gstreamer: pipeline state set to playing", Backend::Info, this); m_tickTimer->start(); changeState(Phonon::PlayingState); if (m_resumeState && m_oldState == Phonon::PlayingState) { seek(m_oldPos); m_resumeState = false; } break; case GST_STATE_NULL: m_backend->logMessage("gstreamer: pipeline state set to null", Backend::Info, this); m_tickTimer->stop(); break; case GST_STATE_PAUSED : m_backend->logMessage("gstreamer: pipeline state set to paused", Backend::Info, this); m_tickTimer->start(); if (state() == Phonon::LoadingState) { // No_more_pads is not emitted from the decodebin in older versions (0.10.4) noMorePadsAvailable(); loadingComplete(); } else if (m_resumeState && m_oldState == Phonon::PausedState) { changeState(Phonon::PausedState); m_resumeState = false; break; } else { // A lot of autotests can break if we allow all paused changes through. if (m_pendingState == Phonon::PausedState) { changeState(Phonon::PausedState); } } break; case GST_STATE_READY : if (!m_loading && m_pendingState == Phonon::StoppedState) changeState(Phonon::StoppedState); m_backend->logMessage("gstreamer: pipeline state set to ready", Backend::Debug, this); m_tickTimer->stop(); break; case GST_STATE_VOID_PENDING : m_backend->logMessage("gstreamer: pipeline state set to pending (void)", Backend::Debug, this); m_tickTimer->stop(); break; } break; } case GST_MESSAGE_ERROR: { gchar *debug; GError *err; QString logMessage; gst_message_parse_error (gstMessage, &err, &debug); gchar *errorMessage = gst_error_get_message (err->domain, err->code); logMessage.sprintf("Error: %s Message:%s (%s) Code:%d", debug, err->message, errorMessage, err->code); m_backend->logMessage(logMessage, Backend::Warning); g_free(errorMessage); g_free (debug); if (err->domain == GST_RESOURCE_ERROR) { if (err->code == GST_RESOURCE_ERROR_NOT_FOUND) { setError(tr("Could not locate media source."), Phonon::FatalError); } else if (err->code == GST_RESOURCE_ERROR_OPEN_READ) { setError(tr("Could not open media source."), Phonon::FatalError); } else if (err->code == GST_RESOURCE_ERROR_BUSY) { // We need to check if this comes from an audio device by looking at sink caps GstPad* sinkPad = gst_element_get_static_pad(GST_ELEMENT(gstMessage->src), "sink"); if (sinkPad) { GstCaps *caps = gst_pad_get_caps (sinkPad); GstStructure *str = gst_caps_get_structure (caps, 0); if (g_strrstr (gst_structure_get_name (str), "audio")) setError(tr("Could not open audio device. The device is already in use."), Phonon::NormalError); else setError(err->message, Phonon::FatalError); gst_caps_unref (caps); gst_object_unref (sinkPad); } } else { setError(QString(err->message), Phonon::FatalError); } } else if (err->domain == GST_STREAM_ERROR) { switch (err->code) { case GST_STREAM_ERROR_WRONG_TYPE: case GST_STREAM_ERROR_TYPE_NOT_FOUND: setError(tr("Could not decode media source."), Phonon::FatalError); break; default: setError(tr("Could not open media source."), Phonon::FatalError); break; } } else { setError(QString(err->message), Phonon::FatalError); } g_error_free (err); break; } case GST_MESSAGE_WARNING: { gchar *debug; GError *err; gst_message_parse_warning(gstMessage, &err, &debug); QString msgString; msgString.sprintf("Warning: %s\nMessage:%s", debug, err->message); m_backend->logMessage(msgString, Backend::Warning); g_free (debug); g_error_free (err); break; } case GST_MESSAGE_ELEMENT: { GstMessage *gstMessage = message.rawMessage(); const GstStructure *gstStruct = gst_message_get_structure(gstMessage); //do not free this if (g_strrstr (gst_structure_get_name (gstStruct), "prepare-xwindow-id")) { MediaNodeEvent videoHandleEvent(MediaNodeEvent::VideoHandleRequest); notify(&videoHandleEvent); } break; } case GST_MESSAGE_DURATION: { m_backend->logMessage("GST_MESSAGE_DURATION", Backend::Debug, this); updateTotalTime(); break; } case GST_MESSAGE_BUFFERING: { gint percent = 0; gst_structure_get_int (gstMessage->structure, "buffer-percent", &percent); //gst_message_parse_buffering was introduced in 0.10.11 if (m_bufferPercent != percent) { emit bufferStatus(percent); m_backend->logMessage(QString("Stream buffering %0").arg(percent), Backend::Debug, this); m_bufferPercent = percent; } if (m_state != Phonon::BufferingState) emit stateChanged(m_state, Phonon::BufferingState); else if (percent == 100) emit stateChanged(Phonon::BufferingState, m_state); break; } //case GST_MESSAGE_INFO: //case GST_MESSAGE_STREAM_STATUS: //case GST_MESSAGE_CLOCK_PROVIDE: //case GST_MESSAGE_NEW_CLOCK: //case GST_MESSAGE_STEP_DONE: //case GST_MESSAGE_LATENCY: only from 0.10.12 //case GST_MESSAGE_ASYNC_DONE: only from 0.10.13 default: break; } } void MediaObject::handleEndOfStream() { // If the stream is not seekable ignore // otherwise chained radio broadcasts would stop if (m_atEndOfStream) return; if (!m_seekable) m_atEndOfStream = true; if (m_autoplayTitles && m_availableTitles > 1 && m_currentTitle < m_availableTitles) { _iface_setCurrentTitle(m_currentTitle + 1); return; } if (m_nextSource.type() != MediaSource::Invalid && m_nextSource.type() != MediaSource::Empty) { // We only emit finish when the queue is actually empty QTimer::singleShot (qMax(0, transitionTime()), this, SLOT(beginPlay())); } else { m_pendingState = Phonon::PausedState; emit finished(); if (!m_seekable) { setState(Phonon::StoppedState); // Note the behavior for live streams is not properly defined // But since we cant seek to 0, we don't have much choice other than stopping // the stream } else { // Only emit paused if the finished signal // did not result in a new state if (m_pendingState == Phonon::PausedState) setState(m_pendingState); } } } // Notifes the pipeline about state changes in the media object void MediaObject::notifyStateChange(Phonon::State newstate, Phonon::State oldstate) { Q_UNUSED(oldstate); MediaNodeEvent event(MediaNodeEvent::StateChanged, &newstate); notify(&event); } #ifndef QT_NO_PHONON_MEDIACONTROLLER //interface management bool MediaObject::hasInterface(Interface iface) const { return iface == AddonInterface::TitleInterface; } QVariant MediaObject::interfaceCall(Interface iface, int command, const QList ¶ms) { if (hasInterface(iface)) { switch (iface) { case TitleInterface: switch (command) { case availableTitles: return _iface_availableTitles(); case title: return _iface_currentTitle(); case setTitle: _iface_setCurrentTitle(params.first().toInt()); break; case autoplayTitles: return m_autoplayTitles; case setAutoplayTitles: m_autoplayTitles = params.first().toBool(); break; } break; default: break; } } return QVariant(); } #endif int MediaObject::_iface_availableTitles() const { return m_availableTitles; } int MediaObject::_iface_currentTitle() const { return m_currentTitle; } void MediaObject::_iface_setCurrentTitle(int title) { GstFormat trackFormat = gst_format_get_by_nick("track"); m_backend->logMessage(QString("setCurrentTitle %0").arg(title), Backend::Info, this); if ((title == m_currentTitle) || (title < 1) || (title > m_availableTitles)) return; m_currentTitle = title; //let's seek to the beginning of the song if (gst_element_seek_simple(m_pipeline, trackFormat, GST_SEEK_FLAG_FLUSH, m_currentTitle - 1)) { updateTotalTime(); m_atEndOfStream = false; emit titleChanged(title); emit totalTimeChanged(totalTime()); } } } // ns Gstreamer } // ns Phonon QT_END_NAMESPACE #include "moc_mediaobject.cpp"