1
/* This file is part of the KDE project.
3
Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies).
5
This library is free software: you can redistribute it and/or modify
6
it under the terms of the GNU Lesser General Public License as published by
7
the Free Software Foundation, either version 2.1 or 3 of the License.
9
This library is distributed in the hope that it will be useful,
10
but WITHOUT ANY WARRANTY; without even the implied warranty of
11
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
GNU Lesser General Public License for more details.
14
You should have received a copy of the GNU Lesser General Public License
15
along with this library. If not, see <http://www.gnu.org/licenses/>.
18
#include <gst/interfaces/navigation.h>
19
#include <gst/interfaces/propertyprobe.h>
20
#include <gst/pbutils/install-plugins.h>
22
#include "mediaobject.h"
23
#include "videowidget.h"
26
#include "streamreader.h"
27
#include "phononsrc.h"
28
#include "phonon-config-gstreamer.h"
30
#include <QtCore/QByteRef>
31
#include <QtCore/QCoreApplication>
32
#include <QtCore/QEvent>
33
#include <QtCore/QFile>
34
#include <QtCore/QLibrary>
35
#include <QtCore/QStringList>
36
#include <QtCore/QTimer>
37
#include <QtCore/QVector>
39
#define ABOUT_TO_FINNISH_TIME 2000
40
#define MAX_QUEUE_TIME 20 * GST_SECOND
49
MediaObject::MediaObject(Backend *backend, QObject *parent)
51
, MediaNode(backend, AudioSource | VideoSource)
52
, m_resumeState(false)
53
, m_oldState(Phonon::LoadingState)
55
, m_state(Phonon::LoadingState)
56
, m_pendingState(Phonon::LoadingState)
57
, m_tickTimer(new QTimer(this))
62
, m_prefinishMarkReachedNotEmitted(true)
63
, m_aboutToFinishEmitted(false)
73
, m_videoStreamFound(false)
76
, m_atEndOfStream(false)
77
, m_atStartOfStream(false)
78
, m_error(Phonon::NoError)
82
, m_previousTickTime(-1)
83
, m_resetNeeded(false)
84
, m_autoplayTitles(true)
85
, m_availableTitles(0)
89
qRegisterMetaType<GstCaps*>("GstCaps*");
90
qRegisterMetaType<State>("State");
93
m_name = "MediaObject" + QString::number(count++);
95
if (!m_backend->isValid()) {
96
setError(tr("Cannot start playback. \n\nCheck your GStreamer installation and make sure you "
97
"\nhave libgstreamer-plugins-base installed."), Phonon::FatalError);
101
m_backend->addBusWatcher(this);
102
connect(m_tickTimer, SIGNAL(timeout()), SLOT(emitTick()));
104
connect(this, SIGNAL(stateChanged(Phonon::State, Phonon::State)),
105
this, SLOT(notifyStateChange(Phonon::State, Phonon::State)));
109
MediaObject::~MediaObject()
111
m_backend->removeBusWatcher(this);
113
gst_element_set_state(m_pipeline, GST_STATE_NULL);
114
gst_object_unref(m_pipeline);
117
gst_element_set_state(m_audioGraph, GST_STATE_NULL);
118
gst_object_unref(m_audioGraph);
121
gst_element_set_state(m_videoGraph, GST_STATE_NULL);
122
gst_object_unref(m_videoGraph);
126
QString stateString(const Phonon::State &state)
129
case Phonon::LoadingState:
130
return QString("LoadingState");
131
case Phonon::StoppedState:
132
return QString("StoppedState");
133
case Phonon::PlayingState:
134
return QString("PlayingState");
135
case Phonon::BufferingState:
136
return QString("BufferingState");
137
case Phonon::PausedState:
138
return QString("PausedState");
139
case Phonon::ErrorState:
140
return QString("ErrorState");
146
pluginInstallationDone( GstInstallPluginsReturn res, gpointer userData )
148
// Nothing inside yet
153
void MediaObject::saveState()
155
//Only first resumeState is respected
159
if (m_pendingState == Phonon::PlayingState || m_pendingState == Phonon::PausedState) {
160
m_resumeState = true;
161
m_oldState = m_pendingState;
162
m_oldPos = getPipelinePos();
166
void MediaObject::resumeState()
169
QMetaObject::invokeMethod(this, "setState", Qt::QueuedConnection, Q_ARG(State, m_oldState));
172
void MediaObject::newPadAvailable (GstPad *pad)
176
caps = gst_pad_get_caps (pad);
178
str = gst_caps_get_structure (caps, 0);
179
QString mediaString(gst_structure_get_name (str));
181
if (mediaString.startsWith("video")) {
183
} else if (mediaString.startsWith("audio")) {
186
m_backend->logMessage("Could not connect pad", Backend::Warning);
188
gst_caps_unref (caps);
192
void MediaObject::cb_newpad (GstElement *decodebin,
202
MediaObject *media = static_cast<MediaObject*>(data);
204
media->newPadAvailable(pad);
207
void MediaObject::noMorePadsAvailable ()
209
if (m_missingCodecs.size() > 0) {
210
bool canPlay = (m_hasAudio || m_videoStreamFound);
211
Phonon::ErrorType error = canPlay ? Phonon::NormalError : Phonon::FatalError;
212
#ifdef PLUGIN_INSTALL_API
213
GstInstallPluginsContext *ctx = gst_install_plugins_context_new();
214
QWidget *activeWindow = QApplication::activeWindow();
216
gst_install_plugins_context_set_xid(ctx, static_cast<int>(activeWindow->winId()));
219
QByteArray missingCodec = m_missingCodecs.first().toLocal8Bit();
220
details[0] = missingCodec.data();
222
GstInstallPluginsReturn status;
224
status = gst_install_plugins_async(details, ctx, pluginInstallationDone, NULL);
225
gst_install_plugins_context_free (ctx);
227
if ( status != GST_INSTALL_PLUGINS_STARTED_OK )
229
if( status == GST_INSTALL_PLUGINS_HELPER_MISSING )
230
setError(QString(tr("Missing codec helper script assistant.")), Phonon::FatalError);
232
setError(QString(tr("Plugin codec installation failed for codec: %1"))
233
.arg(m_missingCodecs[0].split('|')[3]), error);
235
m_missingCodecs.clear();
237
QString codecs = m_missingCodecs.join(", ");
239
if (error == Phonon::NormalError && m_hasVideo && !m_videoStreamFound) {
241
emit hasVideoChanged(false);
243
setError(QString(tr("A required codec is missing. You need to install the following codec(s) to play this content: %0")).arg(codecs), error);
244
m_missingCodecs.clear();
249
void MediaObject::cb_no_more_pads (GstElement * decodebin, gpointer data)
252
MediaObject *media = static_cast<MediaObject*>(data);
254
QMetaObject::invokeMethod(media, "noMorePadsAvailable", Qt::QueuedConnection);
257
typedef void (*Ptr_gst_pb_utils_init)();
258
typedef gchar* (*Ptr_gst_pb_utils_get_codec_description)(const GstCaps *);
260
void MediaObject::cb_unknown_type (GstElement *decodebin, GstPad *pad, GstCaps *caps, gpointer data)
264
MediaObject *media = static_cast<MediaObject*>(data);
267
QString value = "unknown codec";
269
// These functions require GStreamer > 0.10.12
270
#ifndef QT_NO_LIBRARY
271
static Ptr_gst_pb_utils_init p_gst_pb_utils_init = 0;
272
static Ptr_gst_pb_utils_get_codec_description p_gst_pb_utils_get_codec_description = 0;
273
if (!p_gst_pb_utils_init) {
274
p_gst_pb_utils_init = (Ptr_gst_pb_utils_init)QLibrary::resolve(QLatin1String("gstpbutils-0.10"), 0, "gst_pb_utils_init");
275
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");
276
if (p_gst_pb_utils_init)
277
p_gst_pb_utils_init();
279
if (p_gst_pb_utils_get_codec_description) {
280
gchar *codecName = NULL;
281
codecName = p_gst_pb_utils_get_codec_description (caps);
282
value = QString::fromUtf8(codecName);
285
#endif //QT_NO_LIBRARY
287
// For GStreamer versions < 0.10.12
288
GstStructure *str = gst_caps_get_structure (caps, 0);
289
value = QString::fromUtf8(gst_structure_get_name (str));
293
#ifdef PLUGIN_INSTALL_API
294
QString plugins = QString("gstreamer|0.10|%0|%1|decoder-%2")
295
.arg( qApp->applicationName() )
297
.arg( QString::fromUtf8(gst_caps_to_string (caps) ) );
298
media->addMissingCodecName( plugins );
300
media->addMissingCodecName( value );
304
static void notifyVideoCaps(GObject *obj, GParamSpec *, gpointer data)
306
GstPad *pad = GST_PAD(obj);
307
GstCaps *caps = gst_pad_get_caps (pad);
309
MediaObject *media = static_cast<MediaObject*>(data);
311
// We do not want any more notifications until the source changes
312
g_signal_handler_disconnect(pad, media->capsHandler());
314
// setVideoCaps calls loadingComplete(), meaning we cannot call it from
315
// the streaming thread
316
QMetaObject::invokeMethod(media, "setVideoCaps", Qt::QueuedConnection, Q_ARG(GstCaps *, caps));
319
void MediaObject::setVideoCaps(GstCaps *caps)
324
if ((str = gst_caps_get_structure (caps, 0))) {
325
if (gst_structure_get_int (str, "width", &width) && gst_structure_get_int (str, "height", &height)) {
327
gint aspectDenum = 0;
328
if (gst_structure_get_fraction(str, "pixel-aspect-ratio", &aspectNum, &aspectDenum)) {
330
width = width*aspectNum/aspectDenum;
332
// Let child nodes know about our new video size
333
QSize size(width, height);
334
MediaNodeEvent event(MediaNodeEvent::VideoSizeChanged, &size);
338
gst_caps_unref(caps);
341
// Adds an element to the pipeline if not previously added
342
bool MediaObject::addToPipeline(GstElement *elem)
345
if (!GST_ELEMENT_PARENT(elem)) { // If not already in pipeline
346
success = gst_bin_add(GST_BIN(m_pipeline), elem);
351
void MediaObject::connectVideo(GstPad *pad)
353
GstState currentState = GST_STATE(m_pipeline);
354
if (addToPipeline(m_videoGraph)) {
355
GstPad *videopad = gst_element_get_pad (m_videoGraph, "sink");
356
if (!GST_PAD_IS_LINKED (videopad) && (gst_pad_link (pad, videopad) == GST_PAD_LINK_OK)) {
357
gst_element_set_state(m_videoGraph, currentState == GST_STATE_PLAYING ? GST_STATE_PLAYING : GST_STATE_PAUSED);
358
m_videoStreamFound = true;
359
m_backend->logMessage("Video track connected", Backend::Info, this);
360
// Note that the notify::caps _must_ be installed after linking to work with Dapper
361
m_capsHandler = g_signal_connect(pad, "notify::caps", G_CALLBACK(notifyVideoCaps), this);
363
if (!m_loading && !m_hasVideo) {
364
m_hasVideo = m_videoStreamFound;
365
emit hasVideoChanged(m_hasVideo);
368
gst_object_unref (videopad);
370
m_backend->logMessage("The video stream could not be plugged.", Backend::Info, this);
374
void MediaObject::connectAudio(GstPad *pad)
376
GstState currentState = GST_STATE(m_pipeline);
377
if (addToPipeline(m_audioGraph)) {
378
GstPad *audiopad = gst_element_get_pad (m_audioGraph, "sink");
379
if (!GST_PAD_IS_LINKED (audiopad) && (gst_pad_link (pad, audiopad)==GST_PAD_LINK_OK)) {
380
gst_element_set_state(m_audioGraph, currentState == GST_STATE_PLAYING ? GST_STATE_PLAYING : GST_STATE_PAUSED);
382
m_backend->logMessage("Audio track connected", Backend::Info, this);
384
gst_object_unref (audiopad);
386
m_backend->logMessage("The audio stream could not be plugged.", Backend::Info, this);
390
void MediaObject::cb_pad_added(GstElement *decodebin,
395
GstPad *decodepad = static_cast<GstPad*>(data);
396
gst_pad_link (pad, decodepad);
397
//gst_object_unref (decodepad);
401
* Create a media source from a given URL.
403
* returns true if successful
405
bool MediaObject::createPipefromURL(const QUrl &url)
407
// Remove any existing data source
409
gst_bin_remove(GST_BIN(m_pipeline), m_datasource);
410
// m_pipeline has the only ref to datasource
414
// Verify that the uri can be parsed
415
if (!url.isValid()) {
416
m_backend->logMessage(QString("%1 is not a valid URI").arg(url.toString()));
420
// Create a new datasource based on the input URL
421
// add the 'file' scheme if it's missing; the double '/' is needed!
422
QByteArray encoded_cstr_url = (url.scheme() == QLatin1String("") ?
423
"file://" + url.toEncoded() :
425
m_datasource = gst_element_make_from_uri(GST_URI_SRC, encoded_cstr_url.constData(), (const char*)NULL);
429
// Set the device for MediaSource::Disc
430
if (m_source.type() == MediaSource::Disc) {
432
if (g_object_class_find_property (G_OBJECT_GET_CLASS (m_datasource), "device")) {
433
QByteArray mediaDevice = QFile::encodeName(m_source.deviceName());
434
if (!mediaDevice.isEmpty())
435
g_object_set (G_OBJECT (m_datasource), "device", mediaDevice.constData(), (const char*)NULL);
438
// Also Set optical disc speed to 2X for Audio CD
439
if (m_source.discType() == Phonon::Cd
440
&& (g_object_class_find_property (G_OBJECT_GET_CLASS (m_datasource), "read-speed"))) {
441
g_object_set (G_OBJECT (m_datasource), "read-speed", 2, (const char*)NULL);
442
m_backend->logMessage(QString("new device speed : 2X"), Backend::Info, this);
446
/* make HTTP sources send extra headers so we get icecast
447
* metadata in case the stream is an icecast stream */
448
if (encoded_cstr_url.startsWith("http://")
449
&& g_object_class_find_property (G_OBJECT_GET_CLASS (m_datasource), "iradio-mode")) {
450
g_object_set (m_datasource, "iradio-mode", TRUE, NULL);
454
// Link data source into pipeline
455
gst_bin_add(GST_BIN(m_pipeline), m_datasource);
456
if (!gst_element_link(m_datasource, m_decodebin)) {
457
// For sources with dynamic pads (such as RtspSrc) we need to connect dynamically
458
GstPad *decodepad = gst_element_get_pad (m_decodebin, "sink");
459
g_signal_connect (m_datasource, "pad-added", G_CALLBACK (&cb_pad_added), decodepad);
466
* Create a media source from a media stream
468
* returns true if successful
470
bool MediaObject::createPipefromStream(const MediaSource &source)
472
#ifndef QT_NO_PHONON_ABSTRACTMEDIASTREAM
473
// Remove any existing data source
475
gst_bin_remove(GST_BIN(m_pipeline), m_datasource);
476
// m_pipeline has the only ref to datasource
480
m_datasource = GST_ELEMENT(g_object_new(phonon_src_get_type(), NULL));
484
StreamReader *streamReader = new StreamReader(source);
485
g_object_set (G_OBJECT (m_datasource), "iodevice", streamReader, (const char*)NULL);
487
// Link data source into pipeline
488
gst_bin_add(GST_BIN(m_pipeline), m_datasource);
489
if (!gst_element_link(m_datasource, m_decodebin)) {
490
gst_bin_remove(GST_BIN(m_pipeline), m_datasource);
494
#else //QT_NO_PHONON_ABSTRACTMEDIASTREAM
500
void MediaObject::createPipeline()
502
m_pipeline = gst_pipeline_new (NULL);
503
gst_object_ref (GST_OBJECT (m_pipeline));
504
gst_object_sink (GST_OBJECT (m_pipeline));
506
m_decodebin = gst_element_factory_make ("decodebin2", NULL);
507
g_signal_connect (m_decodebin, "new-decoded-pad", G_CALLBACK (&cb_newpad), this);
508
g_signal_connect (m_decodebin, "unknown-type", G_CALLBACK (&cb_unknown_type), this);
509
g_signal_connect (m_decodebin, "no-more-pads", G_CALLBACK (&cb_no_more_pads), this);
511
gst_bin_add(GST_BIN(m_pipeline), m_decodebin);
513
// Create a bin to contain the gst elements for this medianode
515
// Set up audio graph
516
m_audioGraph = gst_bin_new(NULL);
517
gst_object_ref (GST_OBJECT (m_audioGraph));
518
gst_object_sink (GST_OBJECT (m_audioGraph));
520
// Note that these queues are only required for streaming content
521
// And should ideally be created on demand as they will disable
522
// pull-mode access. Also note that the max-size-time are increased to
523
// reduce buffer overruns as these are not gracefully handled at the moment.
524
m_audioPipe = gst_element_factory_make("queue", NULL);
525
g_object_set(G_OBJECT(m_audioPipe), "max-size-time", MAX_QUEUE_TIME, (const char*)NULL);
526
gst_bin_add(GST_BIN(m_audioGraph), m_audioPipe);
527
GstPad *audiopad = gst_element_get_pad (m_audioPipe, "sink");
528
gst_element_add_pad (m_audioGraph, gst_ghost_pad_new ("sink", audiopad));
529
gst_object_unref (audiopad);
531
// Set up video graph
532
m_videoGraph = gst_bin_new(NULL);
533
gst_object_ref (GST_OBJECT (m_videoGraph));
534
gst_object_sink (GST_OBJECT (m_videoGraph));
536
m_videoPipe = gst_element_factory_make("queue", NULL);
537
g_object_set(G_OBJECT(m_videoPipe), "max-size-time", MAX_QUEUE_TIME, (const char*)NULL);
538
gst_bin_add(GST_BIN(m_videoGraph), m_videoPipe);
539
GstPad *videopad = gst_element_get_pad (m_videoPipe, "sink");
540
gst_element_add_pad (m_videoGraph, gst_ghost_pad_new ("sink", videopad));
541
gst_object_unref (videopad);
543
if (m_pipeline && m_decodebin && m_audioGraph && m_videoGraph && m_audioPipe && m_videoPipe)
546
m_backend->logMessage("Could not create pipeline for media object", Backend::Warning);
552
State MediaObject::state() const
560
bool MediaObject::hasVideo() const
568
bool MediaObject::isSeekable() const
576
qint64 MediaObject::currentTime() const
582
case Phonon::PausedState:
583
case Phonon::BufferingState:
584
case Phonon::PlayingState:
585
return getPipelinePos();
586
case Phonon::StoppedState:
587
case Phonon::LoadingState:
589
case Phonon::ErrorState:
598
qint32 MediaObject::tickInterval() const
600
return m_tickInterval;
606
void MediaObject::setTickInterval(qint32 newTickInterval)
608
m_tickInterval = newTickInterval;
609
if (m_tickInterval <= 0)
610
m_tickTimer->setInterval(50);
612
m_tickTimer->setInterval(newTickInterval);
618
void MediaObject::play()
620
setState(Phonon::PlayingState);
621
m_resumeState = false;
627
QString MediaObject::errorString() const
629
return m_errorString;
635
Phonon::ErrorType MediaObject::errorType() const
641
* Set the current state of the mediaObject.
643
* !### Note that both Playing and Paused states are set immediately
644
* This should obviously be done in response to actual gstreamer state changes
646
void MediaObject::setState(State newstate)
651
if (m_state == newstate)
655
// We are still loading. The state will be requested
656
// when loading has completed.
657
m_pendingState = newstate;
661
GstState currentState;
662
gst_element_get_state (m_pipeline, ¤tState, NULL, 1000);
665
case Phonon::BufferingState:
666
m_backend->logMessage("phonon state request: buffering", Backend::Info, this);
669
case Phonon::PausedState:
670
m_backend->logMessage("phonon state request: paused", Backend::Info, this);
671
if (currentState == GST_STATE_PAUSED) {
672
changeState(Phonon::PausedState);
673
} else if (gst_element_set_state(m_pipeline, GST_STATE_PAUSED) != GST_STATE_CHANGE_FAILURE) {
674
m_pendingState = Phonon::PausedState;
676
m_backend->logMessage("phonon state request failed", Backend::Info, this);
680
case Phonon::StoppedState:
681
m_backend->logMessage("phonon state request: Stopped", Backend::Info, this);
682
if (currentState == GST_STATE_READY) {
683
changeState(Phonon::StoppedState);
684
} else if (gst_element_set_state(m_pipeline, GST_STATE_READY) != GST_STATE_CHANGE_FAILURE) {
685
m_pendingState = Phonon::StoppedState;
687
m_backend->logMessage("phonon state request failed", Backend::Info, this);
689
m_atEndOfStream = false;
692
case Phonon::PlayingState:
694
// ### Note this is a workaround and it should really be gracefully
695
// handled by medianode when we implement live connections.
696
// This generally happens if medianodes have been connected after the MediaSource was set
697
// Note that a side-effect of this is that we resend all meta data.
698
gst_element_set_state(m_pipeline, GST_STATE_NULL);
699
m_resetNeeded = false;
700
// Send a source change so the X11 renderer
701
// will re-set the overlay
702
MediaNodeEvent event(MediaNodeEvent::SourceChanged);
705
m_backend->logMessage("phonon state request: Playing", Backend::Info, this);
706
if (m_atEndOfStream) {
707
m_backend->logMessage("EOS already reached", Backend::Info, this);
708
} else if (currentState == GST_STATE_PLAYING) {
709
changeState(Phonon::PlayingState);
710
} else if (gst_element_set_state(m_pipeline, GST_STATE_PLAYING) != GST_STATE_CHANGE_FAILURE) {
711
m_pendingState = Phonon::PlayingState;
713
m_backend->logMessage("phonon state request failed", Backend::Info, this);
717
case Phonon::ErrorState:
718
m_backend->logMessage("phonon state request : Error", Backend::Warning, this);
719
m_backend->logMessage(QString("Last error : %0").arg(errorString()) , Backend::Warning, this);
720
changeState(Phonon::ErrorState); //immediately set error state
723
case Phonon::LoadingState:
724
m_backend->logMessage("phonon state request: Loading", Backend::Info, this);
725
changeState(Phonon::LoadingState);
731
* Signals that the requested state has completed
732
* by emitting stateChanged and updates the internal state.
734
void MediaObject::changeState(State newstate)
736
if (newstate == m_state)
739
Phonon::State oldState = m_state;
740
m_state = newstate; // m_state must be set before emitting, since
741
// Error state requires that state() will return the new value
742
m_pendingState = newstate;
743
emit stateChanged(newstate, oldState);
746
case Phonon::PausedState:
747
m_backend->logMessage("phonon state changed: paused", Backend::Info, this);
750
case Phonon::BufferingState:
751
m_backend->logMessage("phonon state changed: buffering", Backend::Info, this);
754
case Phonon::PlayingState:
755
m_backend->logMessage("phonon state changed: Playing", Backend::Info, this);
758
case Phonon::StoppedState:
759
m_backend->logMessage("phonon state changed: Stopped", Backend::Info, this);
760
// We must reset the pipeline when playing again
761
m_resetNeeded = true;
765
case Phonon::ErrorState:
767
m_backend->logMessage("phonon state changed : Error", Backend::Info, this);
768
m_backend->logMessage(errorString(), Backend::Warning, this);
771
case Phonon::LoadingState:
772
m_backend->logMessage("phonon state changed: Loading", Backend::Info, this);
777
void MediaObject::setError(const QString &errorString, Phonon::ErrorType error)
779
m_errorString = errorString;
783
if (error == Phonon::FatalError) {
785
emit hasVideoChanged(false);
786
gst_element_set_state(m_pipeline, GST_STATE_READY);
787
changeState(Phonon::ErrorState);
789
if (m_loading) //Flag error only after loading has completed
790
m_pendingState = Phonon::ErrorState;
792
changeState(Phonon::ErrorState);
796
qint64 MediaObject::totalTime() const
801
qint32 MediaObject::prefinishMark() const
803
return m_prefinishMark;
806
qint32 MediaObject::transitionTime() const
808
return m_transitionTime;
811
void MediaObject::setTransitionTime(qint32 time)
813
m_transitionTime = time;
816
qint64 MediaObject::remainingTime() const
818
return totalTime() - currentTime();
821
MediaSource MediaObject::source() const
826
void MediaObject::setNextSource(const MediaSource &source)
828
if (source.type() == MediaSource::Invalid &&
829
source.type() == MediaSource::Empty)
831
m_nextSource = source;
835
* Update total time value from the pipeline
837
bool MediaObject::updateTotalTime()
839
GstFormat format = GST_FORMAT_TIME;
841
if (gst_element_query_duration (GST_ELEMENT(m_pipeline), &format, &duration)) {
842
setTotalTime(duration / GST_MSECOND);
849
* Checks if the current source is seekable
851
void MediaObject::updateSeekable()
859
query = gst_query_new_seeking(GST_FORMAT_TIME);
860
result = gst_element_query (m_pipeline, query);
864
gst_query_parse_seeking (query, &format, &seekable, &start, &stop);
866
if (m_seekable != seekable) {
867
m_seekable = seekable;
868
emit seekableChanged(m_seekable);
872
m_backend->logMessage("Stream is seekable", Backend::Info, this);
874
m_backend->logMessage("Stream is non-seekable", Backend::Info, this);
876
m_backend->logMessage("updateSeekable query failed", Backend::Info, this);
878
gst_query_unref (query);
881
qint64 MediaObject::getPipelinePos() const
883
Q_ASSERT(m_pipeline);
885
// Note some formats (usually mpeg) do not allow us to accurately seek to the
886
// beginning or end of the file so we 'fake' it here rather than exposing the front end to potential issues.
889
if (m_atStartOfStream)
891
if (m_posAtSeek >= 0)
895
GstFormat format = GST_FORMAT_TIME;
896
gst_element_query_position (GST_ELEMENT(m_pipeline), &format, &pos);
897
return (pos / GST_MSECOND);
901
* Internal method to set a new total time for the media object
903
void MediaObject::setTotalTime(qint64 newTime)
906
if (newTime == m_totalTime)
909
m_totalTime = newTime;
911
emit totalTimeChanged(m_totalTime);
917
void MediaObject::setSource(const MediaSource &source)
922
// We have to reset the state completely here, otherwise
923
// remnants of the old pipeline can result in strangenes
924
// such as failing duration queries etc
926
gst_element_set_state(m_pipeline, GST_STATE_NULL);
927
gst_element_get_state(m_pipeline, &state, NULL, 2000);
930
emit currentSourceChanged(m_source);
931
m_previousTickTime = -1;
932
m_missingCodecs.clear();
934
// Go into to loading state
935
changeState(Phonon::LoadingState);
937
// IMPORTANT: Honor the m_resetNeeded flag as it currently stands.
938
// See https://qa.mandriva.com/show_bug.cgi?id=56807
939
//m_resetNeeded = false;
940
m_resumeState = false;
941
m_pendingState = Phonon::StoppedState;
943
// Make sure we start out unconnected
944
if (GST_ELEMENT_PARENT(m_audioGraph))
945
gst_bin_remove(GST_BIN(m_pipeline), m_audioGraph);
946
if (GST_ELEMENT_PARENT(m_videoGraph))
947
gst_bin_remove(GST_BIN(m_pipeline), m_videoGraph);
949
// Clear any existing errors
950
m_aboutToFinishEmitted = false;
952
m_errorString.clear();
955
m_prefinishMarkReachedNotEmitted = true;
956
m_aboutToFinishEmitted = false;
958
m_videoStreamFound = false;
960
m_atEndOfStream = false;
962
m_availableTitles = 0;
966
// Clear existing meta tags
970
switch (source.type()) {
971
case MediaSource::Url: {
972
if (!createPipefromURL(source.url()))
973
setError(tr("Could not open media source."));
977
case MediaSource::LocalFile: {
978
if (!createPipefromURL(QUrl::fromLocalFile(source.fileName())))
979
setError(tr("Could not open media source."));
983
case MediaSource::Invalid:
984
setError(tr("Invalid source type."), Phonon::NormalError);
987
case MediaSource::Empty:
990
case MediaSource::Stream:
991
if (!createPipefromStream(source))
992
setError(tr("Could not open media source."));
995
case MediaSource::Disc:
998
switch (source.discType()) {
1000
qWarning() << "I should never get to see a MediaSource that is a disc but doesn't specify which one";
1002
case Phonon::Cd: // CD tracks can be specified by setting the url in the following way uri=cdda:4
1003
mediaUrl = QLatin1String("cdda://");
1006
mediaUrl = QLatin1String("dvd://");
1009
mediaUrl = QLatin1String("vcd://");
1012
qWarning() << "media " << source.discType() << " not implemented";
1015
if (mediaUrl.isEmpty() || !createPipefromURL(QUrl(mediaUrl)))
1016
setError(tr("Could not open media source."));
1021
m_backend->logMessage("Source type not currently supported", Backend::Warning, this);
1022
setError(tr("Could not open media source."), Phonon::NormalError);
1026
MediaNodeEvent event(MediaNodeEvent::SourceChanged);
1029
// We need to link this node to ensure that fake sinks are connected
1030
// before loading, otherwise the stream will be blocked
1035
void MediaObject::beginLoad()
1037
if (gst_element_set_state(m_pipeline, GST_STATE_PAUSED) != GST_STATE_CHANGE_FAILURE) {
1038
m_backend->logMessage("Begin source load", Backend::Info, this);
1040
setError(tr("Could not open media source."));
1044
// Called when we are ready to leave the loading state
1045
void MediaObject::loadingComplete()
1047
if (m_videoStreamFound) {
1048
MediaNodeEvent event(MediaNodeEvent::VideoAvailable);
1054
setState(m_pendingState);
1055
emit metaDataChanged(m_metaData);
1058
void MediaObject::getStreamInfo()
1063
if (m_videoStreamFound != m_hasVideo) {
1064
m_hasVideo = m_videoStreamFound;
1065
emit hasVideoChanged(m_hasVideo);
1068
if (m_source.discType() == Phonon::Cd) {
1070
GstFormat format = gst_format_get_by_nick("track");
1071
if (gst_element_query_duration (m_pipeline, &format, &titleCount)) {
1072
//check if returned format is still "track",
1073
//gstreamer sometimes returns the total time, if tracks information is not available.
1074
if (qstrcmp(gst_format_get_name(format), "track") == 0) {
1075
int oldAvailableTitles = m_availableTitles;
1076
m_availableTitles = (int)titleCount;
1077
if (m_availableTitles != oldAvailableTitles) {
1078
emit availableTitlesChanged(m_availableTitles);
1079
m_backend->logMessage(QString("Available titles changed: %0").arg(m_availableTitles), Backend::Info, this);
1086
void MediaObject::setPrefinishMark(qint32 newPrefinishMark)
1088
m_prefinishMark = newPrefinishMark;
1089
if (currentTime() < totalTime() - m_prefinishMark) // not about to finish
1090
m_prefinishMarkReachedNotEmitted = true;
1093
void MediaObject::pause()
1095
m_backend->logMessage("pause()", Backend::Info, this);
1096
if (state() != Phonon::PausedState)
1097
setState(Phonon::PausedState);
1098
m_resumeState = false;
1101
void MediaObject::stop()
1103
if (state() != Phonon::StoppedState) {
1104
setState(Phonon::StoppedState);
1105
m_prefinishMarkReachedNotEmitted = true;
1107
m_resumeState = false;
1110
void MediaObject::seek(qint64 time)
1117
case Phonon::PlayingState:
1118
case Phonon::StoppedState:
1119
case Phonon::PausedState:
1120
case Phonon::BufferingState:
1121
m_backend->logMessage(QString("Seek to pos %0").arg(time), Backend::Info, this);
1124
m_atStartOfStream = true;
1126
m_atStartOfStream = false;
1128
m_posAtSeek = getPipelinePos();
1129
m_tickTimer->stop();
1131
if (gst_element_seek(m_pipeline, 1.0, GST_FORMAT_TIME,
1132
GST_SEEK_FLAG_FLUSH, GST_SEEK_TYPE_SET,
1133
time * GST_MSECOND, GST_SEEK_TYPE_NONE, GST_CLOCK_TIME_NONE))
1135
case Phonon::LoadingState:
1136
case Phonon::ErrorState:
1140
quint64 current = currentTime();
1141
quint64 total = totalTime();
1143
if (current < total - m_prefinishMark)
1144
m_prefinishMarkReachedNotEmitted = true;
1145
if (current < total - ABOUT_TO_FINNISH_TIME)
1146
m_aboutToFinishEmitted = false;
1147
m_atEndOfStream = false;
1151
void MediaObject::emitTick()
1153
if (m_resumeState) {
1157
qint64 currentTime = getPipelinePos();
1158
qint64 totalTime = m_totalTime;
1160
if (m_tickInterval > 0 && currentTime != m_previousTickTime) {
1161
emit tick(currentTime);
1162
m_previousTickTime = currentTime;
1164
if (m_state == Phonon::PlayingState) {
1165
if (currentTime >= totalTime - m_prefinishMark) {
1166
if (m_prefinishMarkReachedNotEmitted) {
1167
m_prefinishMarkReachedNotEmitted = false;
1168
emit prefinishMarkReached(totalTime - currentTime);
1171
// Prepare load of next source
1172
if (currentTime >= totalTime - ABOUT_TO_FINNISH_TIME) {
1173
if (m_source.type() == MediaSource::Disc &&
1175
m_availableTitles > 1 &&
1176
m_currentTitle < m_availableTitles) {
1177
m_aboutToFinishEmitted = false;
1178
} else if (!m_aboutToFinishEmitted) {
1179
m_aboutToFinishEmitted = true; // track is about to finish
1180
emit aboutToFinish();
1188
* Used to iterate through the gst_tag_list and extract values
1190
void foreach_tag_function(const GstTagList *list, const gchar *tag, gpointer user_data)
1192
TagMap *newData = static_cast<TagMap *>(user_data);
1194
GType type = gst_tag_get_type(tag);
1196
case G_TYPE_STRING: {
1198
gst_tag_list_get_string(list, tag, &str);
1199
value = QString::fromUtf8(str);
1204
case G_TYPE_BOOLEAN: {
1206
gst_tag_list_get_boolean(list, tag, &bval);
1207
value = QString::number(bval);
1213
gst_tag_list_get_int(list, tag, &ival);
1214
value = QString::number(ival);
1220
gst_tag_list_get_uint(list, tag, &uival);
1221
value = QString::number(uival);
1225
case G_TYPE_FLOAT: {
1227
gst_tag_list_get_float(list, tag, &fval);
1228
value = QString::number(fval);
1232
case G_TYPE_DOUBLE: {
1234
gst_tag_list_get_double(list, tag, &dval);
1235
value = QString::number(dval);
1240
//qDebug("Unsupported tag type: %s", g_type_name(type));
1244
QString key = QString(tag).toUpper();
1245
QString currVal = newData->value(key);
1246
if (!value.isEmpty() && !(newData->contains(key) && currVal == value))
1247
newData->insert(key, value);
1251
* Triggers playback after a song has completed in the current media queue
1253
void MediaObject::beginPlay()
1255
setSource(m_nextSource);
1256
m_nextSource = MediaSource();
1257
m_pendingState = Phonon::PlayingState;
1261
* Handle GStreamer bus messages
1263
void MediaObject::handleBusMessage(const Message &message)
1269
GstMessage *gstMessage = message.rawMessage();
1270
Q_ASSERT(m_pipeline);
1272
if (m_backend->debugLevel() >= Backend::Debug) {
1273
int type = GST_MESSAGE_TYPE(gstMessage);
1274
gchar* name = gst_element_get_name(gstMessage->src);
1275
QString msgString = QString("Bus: %0 (%1)").arg(gst_message_type_get_name ((GstMessageType)type)).arg(name);
1277
m_backend->logMessage(msgString, Backend::Debug, this);
1280
switch (GST_MESSAGE_TYPE (gstMessage)) {
1282
case GST_MESSAGE_EOS:
1283
m_backend->logMessage("EOS received", Backend::Info, this);
1284
handleEndOfStream();
1287
case GST_MESSAGE_TAG: {
1288
GstTagList* tag_list = 0;
1289
gst_message_parse_tag(gstMessage, &tag_list);
1292
gst_tag_list_foreach (tag_list, &foreach_tag_function, &newTags);
1293
gst_tag_list_free(tag_list);
1295
// Determine if we should no fake the album/artist tags.
1296
// This is a little confusing as we want to fake it on initial
1297
// connection where title, album and artist are all missing.
1298
// There are however times when we get just other information,
1299
// e.g. codec, and so we want to only do clever stuff if we
1300
// have a commonly available tag (ORGANIZATION) or we have a
1304
&& ((!newTags.contains("TITLE")
1305
&& newTags.contains("ORGANIZATION"))
1306
|| (newTags.contains("TITLE")
1307
&& m_metaData.value("TITLE") != newTags.value("TITLE")))
1308
&& !newTags.contains("ALBUM")
1309
&& !newTags.contains("ARTIST"));
1311
TagMap oldMap = m_metaData; // Keep a copy of the old one for reference
1313
// Now we've checked the new data, append any new meta tags to the existing tag list
1314
// We cannot use TagMap::iterator as this is a multimap and when streaming data
1315
// could in theory be lost.
1316
QList<QString> keys = newTags.keys();
1317
for (QList<QString>::iterator i = keys.begin(); i != keys.end(); ++i) {
1320
// If we're streaming, we need to remove data in m_metaData
1321
// in order to stop it filling up indefinitely (as it's a multimap)
1322
m_metaData.remove(key);
1324
QList<QString> values = newTags.values(key);
1325
for (QList<QString>::iterator j = values.begin(); j != values.end(); ++j) {
1327
QString currVal = m_metaData.value(key);
1328
if (!m_metaData.contains(key) || currVal != value) {
1329
m_metaData.insert(key, value);
1334
m_backend->logMessage("Meta tags found", Backend::Info, this);
1335
if (oldMap != m_metaData) {
1336
// This is a bit of a hack to ensure that stream metadata is
1337
// returned. We get as much as we can from the Shoutcast server's
1338
// StreamTitle= header. If further info is decoded from the stream
1339
// itself later, then it will overwrite this info.
1340
if (m_isStream && fake_it) {
1341
m_metaData.remove("ALBUM");
1342
m_metaData.remove("ARTIST");
1344
// Detect whether we want to "fill in the blanks"
1346
if (m_metaData.contains("TITLE"))
1348
str = m_metaData.value("TITLE");
1350
// Check to see if our title matches "%s - %s"
1351
// Where neither %s are empty...
1352
if ((splitpoint = str.indexOf(" - ")) > 0
1353
&& str.size() > (splitpoint+3)) {
1354
m_metaData.insert("ARTIST", str.left(splitpoint));
1355
m_metaData.replace("TITLE", str.mid(splitpoint+3));
1358
str = m_metaData.value("GENRE");
1360
m_metaData.insert("TITLE", str);
1362
m_metaData.insert("TITLE", "Streaming Data");
1364
if (!m_metaData.contains("ARTIST")) {
1365
str = m_metaData.value("LOCATION");
1367
m_metaData.insert("ARTIST", str);
1369
m_metaData.insert("ARTIST", "Streaming Data");
1371
str = m_metaData.value("ORGANIZATION");
1373
m_metaData.insert("ALBUM", str);
1375
m_metaData.insert("ALBUM", "Streaming Data");
1377
// As we manipulate the title, we need to recompare
1378
// oldMap and m_metaData here...
1379
if (oldMap != m_metaData && !m_loading)
1380
emit metaDataChanged(m_metaData);
1386
case GST_MESSAGE_STATE_CHANGED : {
1388
if (gstMessage->src != GST_OBJECT(m_pipeline))
1393
GstState pendingState;
1394
gst_message_parse_state_changed (gstMessage, &oldState, &newState, &pendingState);
1396
if (newState == pendingState)
1403
case GST_STATE_PLAYING :
1404
m_atStartOfStream = false;
1405
m_backend->logMessage("gstreamer: pipeline state set to playing", Backend::Info, this);
1406
m_tickTimer->start();
1407
changeState(Phonon::PlayingState);
1408
if ((m_source.type() == MediaSource::Disc) && (m_currentTitle != m_pendingTitle)) {
1409
setTrack(m_pendingTitle);
1411
if (m_resumeState && m_oldState == Phonon::PlayingState) {
1413
m_resumeState = false;
1417
case GST_STATE_NULL:
1418
m_backend->logMessage("gstreamer: pipeline state set to null", Backend::Info, this);
1419
m_tickTimer->stop();
1422
case GST_STATE_PAUSED :
1423
m_backend->logMessage("gstreamer: pipeline state set to paused", Backend::Info, this);
1424
m_tickTimer->start();
1425
if (state() == Phonon::LoadingState) {
1426
// No_more_pads is not emitted from the decodebin in older versions (0.10.4)
1427
noMorePadsAvailable();
1429
} else if (m_resumeState && m_oldState == Phonon::PausedState) {
1430
changeState(Phonon::PausedState);
1431
m_resumeState = false;
1434
// A lot of autotests can break if we allow all paused changes through.
1435
if (m_pendingState == Phonon::PausedState) {
1436
changeState(Phonon::PausedState);
1441
case GST_STATE_READY :
1442
if (!m_loading && m_pendingState == Phonon::StoppedState)
1443
changeState(Phonon::StoppedState);
1444
m_backend->logMessage("gstreamer: pipeline state set to ready", Backend::Debug, this);
1445
m_tickTimer->stop();
1446
if ((m_source.type() == MediaSource::Disc) && (m_currentTitle != m_pendingTitle)) {
1447
setTrack(m_pendingTitle);
1451
case GST_STATE_VOID_PENDING :
1452
m_backend->logMessage("gstreamer: pipeline state set to pending (void)", Backend::Debug, this);
1453
m_tickTimer->stop();
1459
case GST_MESSAGE_ERROR: {
1463
gst_message_parse_error (gstMessage, &err, &debug);
1464
gchar *errorMessage = gst_error_get_message (err->domain, err->code);
1465
logMessage.sprintf("Error: %s Message:%s (%s) Code:%d", debug, err->message, errorMessage, err->code);
1466
m_backend->logMessage(logMessage, Backend::Warning);
1467
g_free(errorMessage);
1470
if (err->domain == GST_RESOURCE_ERROR) {
1471
if (err->code == GST_RESOURCE_ERROR_NOT_FOUND) {
1472
setError(tr("Could not locate media source."), Phonon::FatalError);
1473
} else if (err->code == GST_RESOURCE_ERROR_OPEN_READ) {
1474
setError(tr("Could not open media source."), Phonon::FatalError);
1475
} else if (err->code == GST_RESOURCE_ERROR_BUSY) {
1476
// We need to check if this comes from an audio device by looking at sink caps
1477
GstPad* sinkPad = gst_element_get_static_pad(GST_ELEMENT(gstMessage->src), "sink");
1479
GstCaps *caps = gst_pad_get_caps (sinkPad);
1480
GstStructure *str = gst_caps_get_structure (caps, 0);
1481
if (g_strrstr (gst_structure_get_name (str), "audio"))
1482
setError(tr("Could not open audio device. The device is already in use."), Phonon::NormalError);
1484
setError(err->message, Phonon::FatalError);
1485
gst_caps_unref (caps);
1486
gst_object_unref (sinkPad);
1489
setError(QString(err->message), Phonon::FatalError);
1491
} else if (err->domain == GST_STREAM_ERROR) {
1492
switch (err->code) {
1493
case GST_STREAM_ERROR_WRONG_TYPE:
1494
case GST_STREAM_ERROR_TYPE_NOT_FOUND:
1495
setError(tr("Could not decode media source."), Phonon::FatalError);
1498
setError(tr("Could not open media source."), Phonon::FatalError);
1502
setError(QString(err->message), Phonon::FatalError);
1508
case GST_MESSAGE_WARNING: {
1511
gst_message_parse_warning(gstMessage, &err, &debug);
1513
msgString.sprintf("Warning: %s\nMessage:%s", debug, err->message);
1514
m_backend->logMessage(msgString, Backend::Warning);
1520
case GST_MESSAGE_ELEMENT: {
1521
GstMessage *gstMessage = message.rawMessage();
1522
const GstStructure *gstStruct = gst_message_get_structure(gstMessage); //do not free this
1523
if (g_strrstr (gst_structure_get_name (gstStruct), "prepare-xwindow-id")) {
1524
MediaNodeEvent videoHandleEvent(MediaNodeEvent::VideoHandleRequest);
1525
notify(&videoHandleEvent);
1530
case GST_MESSAGE_DURATION: {
1531
m_backend->logMessage("GST_MESSAGE_DURATION", Backend::Debug, this);
1536
case GST_MESSAGE_BUFFERING: {
1538
gst_structure_get_int (gstMessage->structure, "buffer-percent", &percent); //gst_message_parse_buffering was introduced in 0.10.11
1540
if (m_bufferPercent != percent) {
1541
emit bufferStatus(percent);
1542
m_backend->logMessage(QString("Stream buffering %0").arg(percent), Backend::Debug, this);
1543
m_bufferPercent = percent;
1546
if (m_state != Phonon::BufferingState)
1547
emit stateChanged(m_state, Phonon::BufferingState);
1548
else if (percent == 100)
1549
emit stateChanged(Phonon::BufferingState, m_state);
1552
//case GST_MESSAGE_INFO:
1553
//case GST_MESSAGE_STREAM_STATUS:
1554
//case GST_MESSAGE_CLOCK_PROVIDE:
1555
//case GST_MESSAGE_NEW_CLOCK:
1556
//case GST_MESSAGE_STEP_DONE:
1557
//case GST_MESSAGE_LATENCY: only from 0.10.12
1558
//case GST_MESSAGE_ASYNC_DONE: only from 0.10.13
1563
switch (gst_navigation_message_get_type(gstMessage)) {
1564
case GST_NAVIGATION_MESSAGE_MOUSE_OVER: {
1566
if (!gst_navigation_message_parse_mouse_over(gstMessage, &active)) {
1569
MediaNodeEvent mouseOverEvent(MediaNodeEvent::VideoMouseOver, &active);
1570
notify(&mouseOverEvent);
1579
void MediaObject::handleEndOfStream()
1581
// If the stream is not seekable ignore
1582
// otherwise chained radio broadcasts would stop
1585
if (m_atEndOfStream)
1589
m_atEndOfStream = true;
1591
if (m_source.type() == MediaSource::Disc &&
1593
m_availableTitles > 1 &&
1594
m_currentTitle < m_availableTitles) {
1595
_iface_setCurrentTitle(m_currentTitle + 1);
1599
if (m_nextSource.type() != MediaSource::Invalid
1600
&& m_nextSource.type() != MediaSource::Empty) { // We only emit finish when the queue is actually empty
1601
QTimer::singleShot (qMax(0, transitionTime()), this, SLOT(beginPlay()));
1603
m_pendingState = Phonon::PausedState;
1606
setState(Phonon::StoppedState);
1607
// Note the behavior for live streams is not properly defined
1608
// But since we cant seek to 0, we don't have much choice other than stopping
1611
// Only emit paused if the finished signal
1612
// did not result in a new state
1613
if (m_pendingState == Phonon::PausedState)
1614
setState(m_pendingState);
1619
void MediaObject::invalidateGraph()
1621
m_resetNeeded = true;
1622
if (m_state == Phonon::PlayingState || m_state == Phonon::PausedState) {
1623
changeState(Phonon::StoppedState);
1627
// Notifes the pipeline about state changes in the media object
1628
void MediaObject::notifyStateChange(Phonon::State newstate, Phonon::State oldstate)
1631
MediaNodeEvent event(MediaNodeEvent::StateChanged, &newstate);
1635
#ifndef QT_NO_PHONON_MEDIACONTROLLER
1636
//interface management
1637
bool MediaObject::hasInterface(Interface iface) const
1639
return iface == AddonInterface::TitleInterface;
1642
QVariant MediaObject::interfaceCall(Interface iface, int command, const QList<QVariant> ¶ms)
1644
if (hasInterface(iface)) {
1648
case TitleInterface:
1651
case availableTitles:
1652
return _iface_availableTitles();
1654
return _iface_currentTitle();
1656
_iface_setCurrentTitle(params.first().toInt());
1658
case autoplayTitles:
1659
return m_autoplayTitles;
1660
case setAutoplayTitles:
1661
m_autoplayTitles = params.first().toBool();
1673
int MediaObject::_iface_availableTitles() const
1675
return m_availableTitles;
1678
int MediaObject::_iface_currentTitle() const
1680
return m_currentTitle;
1683
void MediaObject::_iface_setCurrentTitle(int title)
1685
m_backend->logMessage(QString("setCurrentTitle %0").arg(title), Backend::Info, this);
1686
if ((title == m_currentTitle) || (title == m_pendingTitle))
1689
m_pendingTitle = title;
1691
if (m_state == Phonon::PlayingState || m_state == Phonon::StoppedState) {
1692
setTrack(m_pendingTitle);
1694
setState(Phonon::StoppedState);
1698
void MediaObject::setTrack(int title)
1700
if (((m_state != Phonon::PlayingState) && (m_state != Phonon::StoppedState)) || (title < 1) || (title > m_availableTitles))
1704
//let's seek to the beginning of the song
1705
GstFormat trackFormat = gst_format_get_by_nick("track");
1706
m_backend->logMessage(QString("setTrack %0").arg(title), Backend::Info, this);
1707
if (gst_element_seek_simple(m_pipeline, trackFormat, GST_SEEK_FLAG_FLUSH, title - 1)) {
1708
m_currentTitle = title;
1710
m_atEndOfStream = false;
1711
emit titleChanged(title);
1712
emit totalTimeChanged(totalTime());
1721
#include "moc_mediaobject.cpp"