Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qgstreamermediaplayer.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include <common/qgstreamermediaplayer_p.h>
5
6#include <audio/qgstreameraudiodevice_p.h>
7#include <common/qglist_helper_p.h>
8#include <common/qgst_debug_p.h>
9#include <common/qgstutils_p.h>
10#include <common/qgst_discoverer_p.h>
11#include <common/qgst_play_p.h>
12#include <common/qgstpipeline_p.h>
13#include <common/qgstreameraudiooutput_p.h>
14#include <common/qgstreamermessage_p.h>
15#include <common/qgstreamermetadata_p.h>
16#include <common/qgstreamervideooutput_p.h>
17#include <common/qgstreamervideosink_p.h>
18#include <uri_handler/qgstreamer_qiodevice_handler_p.h>
19#include <qgstreamerformatinfo_p.h>
20
21#include <QtMultimedia/private/qthreadlocalrhi_p.h>
22#include <QtMultimedia/qaudiodevice.h>
23#include <QtConcurrent/qtconcurrentrun.h>
24#include <QtCore/qdeadlinetimer.h>
25#include <QtCore/qdebug.h>
26#include <QtMultimedia/private/qmultimedia_ranges_p.h>
27#include <QtCore/qiodevice.h>
28#include <QtCore/qloggingcategory.h>
29#include <QtCore/qthread.h>
30#include <QtCore/qurl.h>
31#include <QtCore/private/quniquehandle_p.h>
32
33Q_STATIC_LOGGING_CATEGORY(qLcMediaPlayer, "qt.multimedia.player");
34
35QT_BEGIN_NAMESPACE
36namespace ranges = QtMultimediaPrivate::ranges;
37
38namespace {
39
41{
42 using TrackType = QGstreamerMediaPlayer::TrackType;
43
44 QByteArrayView type = caps.at(0).name();
45
46 if (type.startsWith("video/x-raw"))
47 return TrackType::VideoStream;
48 if (type.startsWith("audio/x-raw"))
49 return TrackType::AudioStream;
50 if (type.startsWith("text"))
51 return TrackType::SubtitleStream;
52
53 return std::nullopt;
54}
55
56} // namespace
57
58QFuture<QGstreamerMediaPlayer::DiscoverResult> QGstreamerMediaPlayer::discover(QUrl url)
59{
60 return QtConcurrent::run([url = std::move(url)] {
61 QGst::QGstDiscoverer discoverer;
62 return discoverer.discover(url);
63 });
64}
65
66void QGstreamerMediaPlayer::handleDiscoverResult(const DiscoverResult &discoveryResult,
67 const QUrl &url)
68{
69 using namespace Qt::Literals;
70 using namespace std::chrono;
71 using namespace std::chrono_literals;
72
73 if (discoveryResult) {
74 // Make sure GstPlay is ready if play() is called from slots during discovery
75 qCDebug(qLcMediaPlayer) << "gst_play_set_uri";
76 gst_play_set_uri(m_gstPlay.get(), url.toEncoded().constData());
77
78 m_trackMetaData.fill({});
79 seekableChanged(discoveryResult->isSeekable);
80 if (discoveryResult->duration)
81 m_duration = round<milliseconds>(*discoveryResult->duration);
82 else
83 m_duration = 0ms;
84 durationChanged(m_duration);
85
86 m_metaData = QGst::toContainerMetadata(*discoveryResult);
87
88 videoAvailableChanged(!discoveryResult->videoStreams.empty());
89 audioAvailableChanged(!discoveryResult->audioStreams.empty());
90
91 m_nativeSize.clear();
92 for (const auto &videoInfo : discoveryResult->videoStreams) {
93 m_trackMetaData[0].emplace_back(QGst::toStreamMetadata(videoInfo));
94 QSize nativeSize = QGstUtils::qCalculateFrameSizeGStreamer(videoInfo.size,
95 videoInfo.pixelAspectRatio);
96 m_nativeSize.emplace_back(nativeSize);
97 }
98 for (const auto &audioInfo : discoveryResult->audioStreams)
99 m_trackMetaData[1].emplace_back(QGst::toStreamMetadata(audioInfo));
100 for (const auto &subtitleInfo : discoveryResult->subtitleStreams)
101 m_trackMetaData[2].emplace_back(QGst::toStreamMetadata(subtitleInfo));
102
103 using Key = QMediaMetaData::Key;
104 auto copyKeysToRootMetadata = [&](const QMediaMetaData &reference, QSpan<const Key> keys) {
105 for (QMediaMetaData::Key key : keys) {
106 QVariant referenceValue = reference.value(key);
107 if (referenceValue.isValid())
108 m_metaData.insert(key, referenceValue);
109 }
110 };
111
112 // FIXME: we duplicate some metadata for the first audio / video track
113 // in future we will want to use e.g. the currently selected track
114 if (!m_trackMetaData[0].empty())
115 copyKeysToRootMetadata(m_trackMetaData[0].front(),
116 {
117 Key::HasHdrContent,
118 Key::Orientation,
119 Key::Resolution,
120 Key::VideoBitRate,
121 Key::VideoCodec,
122 Key::VideoFrameRate,
123 });
124
125 if (!m_trackMetaData[1].empty())
126 copyKeysToRootMetadata(m_trackMetaData[1].front(),
127 {
128 Key::AudioBitRate,
129 Key::AudioCodec,
130 });
131
132 if (!m_url.isEmpty())
133 m_metaData.insert(QMediaMetaData::Key::Url, m_url);
134
135 qCDebug(qLcMediaPlayer) << "metadata:" << m_metaData;
136 qCDebug(qLcMediaPlayer) << "video metadata:" << m_trackMetaData[0];
137 qCDebug(qLcMediaPlayer) << "audio metadata:" << m_trackMetaData[1];
138 qCDebug(qLcMediaPlayer) << "subtitle metadata:" << m_trackMetaData[2];
139
140 metaDataChanged();
141 tracksChanged();
142 m_activeTrack = {
143 isVideoAvailable() ? 0 : -1,
144 isAudioAvailable() ? 0 : -1,
145 -1,
146 };
147 updateVideoTrackEnabled();
148 updateAudioTrackEnabled();
149 updateNativeSizeOnVideoOutput();
150 positionChanged(0ms);
151
152 // Handle the last play/pause/stop call made during async media loading.
153 m_hasPendingMedia = false;
154 if (m_requestedPlaybackState) {
155 switch (*m_requestedPlaybackState) {
156 case QMediaPlayer::PlayingState:
157 play();
158 break;
159 case QMediaPlayer::PausedState:
160 pause();
161 break;
162 default:
163 break;
164 }
165 }
166
167 } else {
168 qCDebug(qLcMediaPlayer) << "Discovery error:" << discoveryResult.error();
169 m_resourceErrorState = ResourceErrorState::ErrorOccurred;
170 setInvalidMediaWithError(QMediaPlayer::Error::ResourceError,
171 u"Resource cannot be discovered"_s);
172 m_hasPendingMedia = false;
173 resetStateForEmptyOrInvalidMedia();
174 };
175}
176
177void QGstreamerMediaPlayer::decoderPadAddedCustomSource(const QGstElement &src, const QGstPad &pad)
178{
179 // GStreamer or application thread
180 if (src != decoder)
181 return;
182
183 qCDebug(qLcMediaPlayer) << "Added pad" << pad.name() << "from" << src.name();
184
185 QGstCaps caps = pad.queryCaps();
186
187 std::optional<QGstreamerMediaPlayer::TrackType> type = toTrackType(caps);
188 if (!type)
189 return;
190
191 customPipelinePads[*type] = pad;
192
193 switch (*type) {
194 case VideoStream: {
195 QGstElement sink = gstVideoOutput->gstreamerVideoSink()
198
199 customPipeline.add(sink);
200 pad.link(sink.sink());
201 customPipelineSinks[VideoStream] = sink;
203 return;
204 }
205 case AudioStream: {
206 QGstElement sink = gstAudioOutput ? gstAudioOutput->gstElement()
208 customPipeline.add(sink);
209 pad.link(sink.sink());
210 customPipelineSinks[AudioStream] = sink;
212 return;
213 }
214 case SubtitleStream: {
215 QGstElement sink = gstVideoOutput->gstreamerVideoSink()
218 customPipeline.add(sink);
219 pad.link(sink.sink());
220 customPipelineSinks[SubtitleStream] = sink;
222 return;
223 }
224
225 default:
226 Q_UNREACHABLE();
227 }
228}
229
230void QGstreamerMediaPlayer::decoderPadRemovedCustomSource(const QGstElement &src,
231 const QGstPad &pad)
232{
233 if (src != decoder)
234 return;
235
236 // application thread!
237 Q_ASSERT(thread()->isCurrentThread());
238
239 qCDebug(qLcMediaPlayer) << "Removed pad" << pad.name() << "from" << src.name() << "for stream"
240 << pad.streamId();
241
242 auto found = ranges::find(customPipelinePads, pad);
243 if (found == customPipelinePads.end())
244 return;
245
246 TrackType type = TrackType(found - customPipelinePads.cbegin());
247
248 switch (type) {
249 case VideoStream:
250 case AudioStream:
251 case SubtitleStream: {
252 if (customPipelineSinks[VideoStream]) {
253 customPipeline.stopAndRemoveElements(customPipelineSinks[VideoStream]);
254 customPipelineSinks[VideoStream] = {};
255 }
256 return;
257
258 default:
259 Q_UNREACHABLE();
260 }
261 }
262}
263
264void QGstreamerMediaPlayer::resetStateForEmptyOrInvalidMedia()
265{
266 using namespace std::chrono_literals;
267 m_nativeSize.clear();
268
269 bool metadataNeedsSignal = !m_metaData.isEmpty();
270 bool tracksNeedsSignal = ranges::any_of(m_trackMetaData, [](const auto &container) {
271 return !container.empty();
272 });
273
274 m_metaData.clear();
275 m_trackMetaData.fill({});
276 m_duration = 0ms;
277 seekableChanged(false);
278
279 videoAvailableChanged(false);
280 audioAvailableChanged(false);
281
282 m_activeTrack.fill(-1);
283
284 if (metadataNeedsSignal)
285 metaDataChanged();
286 if (tracksNeedsSignal)
287 tracksChanged();
288}
289
290void QGstreamerMediaPlayer::updateNativeSizeOnVideoOutput()
291{
292 int activeVideoTrack = activeTrack(TrackType::VideoStream);
293 bool hasVideoTrack = activeVideoTrack != -1;
294
295 QSize nativeSize = hasVideoTrack ? m_nativeSize[activeTrack(TrackType::VideoStream)] : QSize{};
296
297 QVariant orientation = hasVideoTrack
298 ? m_trackMetaData[TrackType::VideoStream][activeTrack(TrackType::VideoStream)].value(
299 QMediaMetaData::Key::Orientation)
300 : QVariant{};
301
302 if (orientation.isValid()) {
303 auto rotation = orientation.value<QtVideo::Rotation>();
304 gstVideoOutput->setRotation(rotation);
305 }
306 gstVideoOutput->setNativeSize(nativeSize);
307}
308
309void QGstreamerMediaPlayer::seekToCurrentPosition()
310{
311 qCDebug(qLcMediaPlayer) << "gst_play_seek";
312 gst_play_seek(m_gstPlay.get(), gst_play_get_position(m_gstPlay.get()));
313}
314
315void QGstreamerMediaPlayer::updateVideoTrackEnabled()
316{
317 bool hasTrack = m_activeTrack[TrackType::VideoStream] != -1;
318 bool hasSink = gstVideoOutput->gstreamerVideoSink() != nullptr;
319
320 gstVideoOutput->setActive(hasTrack);
321 gst_play_set_video_track_enabled(m_gstPlay.get(), hasTrack && hasSink);
322}
323
324void QGstreamerMediaPlayer::updateAudioTrackEnabled()
325{
326 bool hasTrack = m_activeTrack[TrackType::AudioStream] != -1;
327 bool hasAudioOut = gstAudioOutput;
328
329 gst_play_set_audio_track_enabled(m_gstPlay.get(), hasTrack && hasAudioOut);
330}
331
332void QGstreamerMediaPlayer::updateBufferProgress(float newProgress)
333{
334 if (qFuzzyIsNull(newProgress - m_bufferProgress))
335 return;
336
337 m_bufferProgress = newProgress;
338 bufferProgressChanged(m_bufferProgress);
339}
340
341void QGstreamerMediaPlayer::disconnectDecoderHandlers()
342{
343 auto handlers = std::initializer_list<QGObjectHandlerScopedConnection *>{ &sourceSetup };
344 for (QGObjectHandlerScopedConnection *handler : handlers)
345 handler->disconnect();
346}
347
348q23::expected<QPlatformMediaPlayer *, QString> QGstreamerMediaPlayer::create(QMediaPlayer *parent)
349{
350 auto videoOutput = QGstreamerVideoOutput::create();
351 if (!videoOutput)
352 return q23::unexpected{ videoOutput.error() };
353
354 return new QGstreamerMediaPlayer(videoOutput.value(), parent);
355}
356
357template <typename T>
358void setSeekAccurate(T *config, gboolean accurate)
359{
360 gst_play_config_set_seek_accurate(config, accurate);
361}
362
363QGstreamerMediaPlayer::QGstreamerMediaPlayer(QGstreamerVideoOutput *videoOutput,
364 QMediaPlayer *parent)
365 : QObject(parent),
366 QPlatformMediaPlayer(parent),
367 gstVideoOutput(videoOutput),
368 m_gstPlay{
369 gst_play_new(nullptr),
370 QGstPlayHandle::HasRef,
371 },
372 m_playbin{
373 GST_PIPELINE_CAST(gst_play_get_pipeline(m_gstPlay.get())),
374 QGstPipeline::HasRef,
375 },
376 m_gstPlayBus{
377 QGstBusHandle{ gst_play_get_message_bus(m_gstPlay.get()), QGstBusHandle::HasRef },
378 }
379{
380#if 1
381 // LATER: remove this hack after meta-freescale decides not to pull in outdated APIs
382
383 // QTBUG-131300: nxp deliberately reverted to an old gst-play API before the gst-play API
384 // stabilized. compare:
385 // https://github.com/nxp-imx/gst-plugins-bad/commit/ff04fa9ca1b79c98e836d8cdb26ac3502dafba41
386
387 // Update: Looks like first unaffected Yocto release series is 5.3 Whinlatter, so this hack can
388 // likely be removed when we drop Boot to Qt support for the LTS 5.0 Scarthgap (EOL April 2028).
389 // Link to first unaffected nxp gstreamer branch:
390 // https://github.com/nxp-imx/gst-plugins-bad/blame/MM_04.10.0_2505_L6.12.20/gst-libs/gst/play/gstplay.c#L4690C9-L4690C9
391 constexpr bool useNxpWorkaround = std::is_same_v<decltype(&gst_play_config_set_seek_accurate),
392 void (*)(GstPlay *, gboolean)>;
393
394 QUniqueGstStructureHandle config{
395 gst_play_get_config(m_gstPlay.get()),
396 };
397
398 if constexpr (useNxpWorkaround)
399 setSeekAccurate(m_gstPlay.get(), true);
400 else
401 setSeekAccurate(config.get(), true);
402
403 gst_play_set_config(m_gstPlay.get(), config.release());
404#else
405 QUniqueGstStructureHandle config{
406 gst_play_get_config(m_gstPlay.get()),
407 };
408 gst_play_config_set_seek_accurate(config.get(), true);
409 gst_play_set_config(m_gstPlay.get(), config.release());
410#endif
411
412 gstVideoOutput->setParent(this);
413
414 // NOTE: Creating a GStreamer video sink to be owned by the media player, any sink created by
415 // user would be a pluggable sink connected to this
416 m_gstVideoSink = new QGstreamerRelayVideoSink(this);
417 gstVideoOutput->setVideoSink(m_gstVideoSink);
418
419 m_playbin.set("video-sink", gstVideoOutput->gstElement());
420 m_playbin.set("text-sink", gstVideoOutput->gstSubtitleElement());
421 m_playbin.set("audio-sink", QGstElement::createFromPipelineDescription("fakesink"));
422
423 m_gstPlayBus.installMessageFilter(this);
424
425 // we start without subtitles
426 gst_play_set_subtitle_track_enabled(m_gstPlay.get(), false);
427
428 sourceSetup = m_playbin.connect("source-setup", GCallback(sourceSetupCallback), this);
429
430 m_activeTrack.fill(-1);
431
432 // TODO: how to detect stalled media?
433}
434
436{
437 using namespace std::chrono_literals;
438 using namespace std::chrono;
439 qCDebug(qLcMediaPlayer) << "~QGstreamerMediaPlayer" << m_callStopInDestructor
440 << m_expectedStoppedMessages << state();
441
442 if (customPipeline)
443 cleanupCustomPipeline();
444
445 m_gstPlayBus.removeMessageFilter(static_cast<QGstreamerBusMessageFilter *>(this));
446
447 if (m_callStopInDestructor) {
448 m_expectedStoppedMessages += 1;
449 qCDebug(qLcMediaPlayer) << "gst_play_stop";
450 gst_play_stop(m_gstPlay.get());
451 }
452
453 if (m_expectedStoppedMessages) {
454 auto processNextMessage = [this](nanoseconds timeout) {
455 QGstreamerMessage message{
456 gst_bus_timed_pop_filtered(m_gstPlayBus.get(), timeout.count(),
457 GST_MESSAGE_APPLICATION), QGstreamerMessage::HasRef
458 };
459 if (!message || !gst_play_is_play_message(message.message()))
460 return;
461 GstPlayMessage type;
462 gst_play_message_parse_type(message.message(), &type);
463 qCDebug(qLcMediaPlayer) << QGstPlayMessageAdaptor{ message };
464 if (type == GST_PLAY_MESSAGE_END_OF_STREAM) {
465 m_expectedStoppedMessages += 1;
466 return;
467 }
468 if (type == GST_PLAY_MESSAGE_STATE_CHANGED) {
469 GstPlayState state;
470 gst_play_message_parse_state_changed(message.message(), &state);
471 if (state == GST_PLAY_STATE_STOPPED) {
472 if (m_expectedStoppedMessages > 0)
473 m_expectedStoppedMessages -= 1;
474 }
475 }
476 };
477
478 constexpr auto timeout = 200ms;
479 QDeadlineTimer deadline{timeout};
480 while (!deadline.hasExpired() && m_expectedStoppedMessages)
481 processNextMessage(deadline.remainingTimeAsDuration());
482
483 if (m_expectedStoppedMessages)
484 qWarning() << "Did not receive expected STOPPED state change messages from GstPlay "
485 "in QGstreamerMediaPlayer destructor:" << m_expectedStoppedMessages;
486 }
487
488 // NOTE: gst_play_stop is not sufficient, un-reffing m_gstPlay can deadlock
489 m_playbin.setStateSync(GST_STATE_NULL);
490
491 m_playbin.set("video-sink", QGstElement::createFromPipelineDescription("fakesink"));
492 m_playbin.set("text-sink", QGstElement::createFromPipelineDescription("fakesink"));
493 m_playbin.set("audio-sink", QGstElement::createFromPipelineDescription("fakesink"));
494
495 gst_bus_set_flushing(m_gstPlayBus.get(), TRUE);
496}
497
498void QGstreamerMediaPlayer::updatePositionFromPipeline()
499{
500 using namespace std::chrono;
501
502 positionChanged(round<milliseconds>(nanoseconds{
503 gst_play_get_position(m_gstPlay.get()),
504 }));
505}
506
508{
509 if (isCustomSource()) {
510 constexpr bool traceBusMessages = true;
511 if (traceBusMessages)
512 qCDebug(qLcMediaPlayer) << "received bus message:" << message;
513
514 switch (message.type()) {
515 case GST_MESSAGE_WARNING:
516 qWarning() << "received bus message:" << message;
517 break;
518
519 case GST_MESSAGE_INFO:
520 qInfo() << "received bus message:" << message;
521 break;
522
523 case GST_MESSAGE_ERROR:
524 qWarning() << "received bus message:" << message;
525 customPipeline.dumpPipelineGraph("GST_MESSAGE_ERROR");
526 break;
527
528 case GST_MESSAGE_LATENCY:
529 customPipeline.recalculateLatency();
530 break;
531
532 default:
533 break;
534 }
535 return false;
536 }
537
538 switch (message.type()) {
539 case GST_MESSAGE_APPLICATION:
540 if (gst_play_is_play_message(message.message()))
541 return processBusMessageApplication(message);
542 return false;
543
544 default:
545 qCDebug(qLcMediaPlayer) << message;
546
547 return false;
548 }
549
550 return false;
551}
552
553bool QGstreamerMediaPlayer::processBusMessageApplication(const QGstreamerMessage &message)
554{
555 using namespace std::chrono;
556 GstPlayMessage type;
557 gst_play_message_parse_type(message.message(), &type);
558 qCDebug(qLcMediaPlayer) << QGstPlayMessageAdaptor{ message };
559
560 switch (type) {
561 case GST_PLAY_MESSAGE_URI_LOADED: {
562 mediaStatusChanged(QMediaPlayer::LoadedMedia);
563 return false;
564 }
565
566 case GST_PLAY_MESSAGE_POSITION_UPDATED: {
567 if (state() == QMediaPlayer::PlaybackState::PlayingState) {
568
569 constexpr bool usePayload = false;
570 if constexpr (usePayload) {
571 GstClockTime position;
572 gst_play_message_parse_position_updated(message.message(), &position);
573 positionChanged(round<milliseconds>(nanoseconds{ position }));
574 } else {
575 GstClockTime position = gst_play_get_position(m_gstPlay.get());
576 positionChanged(round<milliseconds>(nanoseconds{ position }));
577 }
578 }
579 return false;
580 }
581 case GST_PLAY_MESSAGE_DURATION_CHANGED: {
582 GstClockTime duration;
583 gst_play_message_parse_duration_updated(message.message(), &duration);
584 milliseconds durationInMs = round<milliseconds>(nanoseconds{ duration });
585 durationChanged(durationInMs);
586
587 m_metaData.insert(QMediaMetaData::Duration, int(durationInMs.count()));
588 metaDataChanged();
589
590 return false;
591 }
592 case GST_PLAY_MESSAGE_BUFFERING: {
593 guint percent;
594 gst_play_message_parse_buffering_percent(message.message(), &percent);
595 updateBufferProgress(percent * 0.01f);
596 return false;
597 }
598 case GST_PLAY_MESSAGE_STATE_CHANGED: {
599 m_playOrPauseCalledSinceLastStateChangedOrEosMessage = false;
600 GstPlayState state;
601 gst_play_message_parse_state_changed(message.message(), &state);
602
603 switch (state) {
604 case GstPlayState::GST_PLAY_STATE_STOPPED:
605 if (m_expectedStoppedMessages > 0)
606 m_expectedStoppedMessages -= 1;
607
608 if (stateChangeToSkip) {
609 qCDebug(qLcMediaPlayer) << " skipping StoppedState transition";
610
611 stateChangeToSkip -= 1;
612 return false;
613 }
614 stateChanged(QMediaPlayer::StoppedState);
615 updateBufferProgress(0);
616 return false;
617
618 case GstPlayState::GST_PLAY_STATE_PAUSED:
619 stateChanged(QMediaPlayer::PausedState);
620 mediaStatusChanged(QMediaPlayer::BufferedMedia);
621 gstVideoOutput->setActive(true);
622 updateBufferProgress(1);
623 return false;
624 case GstPlayState::GST_PLAY_STATE_BUFFERING:
625 mediaStatusChanged(QMediaPlayer::BufferingMedia);
626 return false;
627 case GstPlayState::GST_PLAY_STATE_PLAYING:
628 stateChanged(QMediaPlayer::PlayingState);
629 mediaStatusChanged(QMediaPlayer::BufferedMedia);
630 gstVideoOutput->setActive(true);
631 updateBufferProgress(1);
632
633 return false;
634 default:
635 return false;
636 }
637 }
638 case GST_PLAY_MESSAGE_MEDIA_INFO_UPDATED: {
639 using namespace QGstPlaySupport;
640
641 QUniqueGstPlayMediaInfoHandle info{};
642 gst_play_message_parse_media_info_updated(message.message(), &info);
643
644 seekableChanged(gst_play_media_info_is_seekable(info.get()));
645
646 const gchar *title = gst_play_media_info_get_title(info.get());
647 m_metaData.insert(QMediaMetaData::Title, QString::fromUtf8(title));
648
649 metaDataChanged();
650 tracksChanged();
651
652 return false;
653 }
654 case GST_PLAY_MESSAGE_END_OF_STREAM: {
655 m_expectedStoppedMessages += 1;
656 // Set m_callStopInDestructor to false except in the edge case where play or pause is called
657 // after reaching EOS, but before we've processed this EOS message
658 if (!m_playOrPauseCalledSinceLastStateChangedOrEosMessage)
659 m_callStopInDestructor = false;
660 m_playOrPauseCalledSinceLastStateChangedOrEosMessage = false;
661
662 if (doLoop()) {
663 positionChanged(m_duration);
664 m_callStopInDestructor = true;
665 qCDebug(qLcMediaPlayer) << "EOS: restarting loop (gst_play_play)";
666 gst_play_play(m_gstPlay.get());
667 positionChanged(0ms);
668
669 // we will still get a GST_PLAY_MESSAGE_STATE_CHANGED message, which we will just ignore
670 // for now
671 stateChangeToSkip += 1;
672 } else {
673 qCDebug(qLcMediaPlayer) << "EOS: done";
674 positionChanged(m_duration);
675 mediaStatusChanged(QMediaPlayer::EndOfMedia);
676 stateChanged(QMediaPlayer::StoppedState);
677 gstVideoOutput->setActive(false);
678 }
679
680 return false;
681 }
682 case GST_PLAY_MESSAGE_ERROR:
683 case GST_PLAY_MESSAGE_WARNING:
684 case GST_PLAY_MESSAGE_VIDEO_DIMENSIONS_CHANGED:
685 case GST_PLAY_MESSAGE_VOLUME_CHANGED:
686 case GST_PLAY_MESSAGE_MUTE_CHANGED:
687 case GST_PLAY_MESSAGE_SEEK_DONE:
688 return false;
689
690 default:
691 Q_UNREACHABLE_RETURN(false);
692 }
693}
694
696{
697 return m_duration.count();
698}
699
700bool QGstreamerMediaPlayer::hasMedia() const
701{
702 return !m_url.isEmpty() || m_stream;
703}
704
705bool QGstreamerMediaPlayer::hasValidMedia() const
706{
707 if (!hasMedia())
708 return false;
709
710 switch (mediaStatus()) {
711 case QMediaPlayer::MediaStatus::NoMedia:
712 case QMediaPlayer::MediaStatus::InvalidMedia:
713 return false;
714
715 default:
716 return true;
717 }
718}
719
721{
722 return m_bufferProgress;
723}
724
726{
727 return QMediaTimeRange();
728}
729
731{
732 return gst_play_get_rate(m_gstPlay.get());
733}
734
736{
737 if (isCustomSource()) {
738 static std::once_flag flag;
739 std::call_once(flag, [] {
740 // CAVEAT: unsynchronised with pipeline state. Potentially prone to race conditions
741 qWarning()
742 << "setPlaybackRate with custom gstreamer pipelines can cause pipeline hangs. "
743 "Use with care";
744 });
745
746 customPipeline.setPlaybackRate(rate);
747 return;
748 }
749
750 if (rate == playbackRate())
751 return;
752
753 qCDebug(qLcMediaPlayer) << "gst_play_set_rate" << rate;
754 gst_play_set_rate(m_gstPlay.get(), rate);
755 playbackRateChanged(rate);
756}
757
759{
760 std::chrono::milliseconds posInMs{ pos };
761
762 setPosition(posInMs);
763}
764
765void QGstreamerMediaPlayer::setPosition(std::chrono::milliseconds pos)
766{
767 using namespace std::chrono;
768
769 if (isCustomSource()) {
770 static std::once_flag flag;
771 std::call_once(flag, [] {
772 // CAVEAT: unsynchronised with pipeline state. Potentially prone to race conditions
773 qWarning() << "setPosition with custom gstreamer pipelines can cause pipeline hangs. "
774 "Use with care";
775 });
776
777 customPipeline.setPosition(pos);
778 return;
779 }
780
781 if (m_hasPendingMedia) {
782 return;
783 }
784
785 qCDebug(qLcMediaPlayer) << "gst_play_seek" << pos;
786 gst_play_seek(m_gstPlay.get(), nanoseconds(pos).count());
787
788 if (mediaStatus() == QMediaPlayer::EndOfMedia)
789 mediaStatusChanged(QMediaPlayer::LoadedMedia);
790 positionChanged(pos);
791}
792
794{
795 if (isCustomSource()) {
796 gstVideoOutput->setActive(true);
797 customPipeline.setState(GST_STATE_PLAYING);
798 stateChanged(QMediaPlayer::PlayingState);
799 return;
800 }
801
802 if (m_hasPendingMedia) {
803 // Async media loading in progress via QGstDiscoverer, m_discoveryHandler will fulfill the
804 // last requested playback state later.
805 m_requestedPlaybackState = QMediaPlayer::PlayingState;
806 return;
807 }
808
809 QMediaPlayer::PlaybackState currentState = state();
810 if (currentState == QMediaPlayer::PlayingState || !hasValidMedia())
811 return;
812
813 if (currentState != QMediaPlayer::PausedState)
814 resetCurrentLoop();
815
816 if (mediaStatus() == QMediaPlayer::EndOfMedia) {
817 positionChanged(0);
818 mediaStatusChanged(QMediaPlayer::LoadedMedia);
819 }
820
821 if (m_pendingSeek) {
822 qCDebug(qLcMediaPlayer) << "gst_play_seek";
823 gst_play_seek(m_gstPlay.get(), m_pendingSeek->count());
824 m_pendingSeek = std::nullopt;
825 }
826
827 gstVideoOutput->setActive(true);
828 m_callStopInDestructor = true;
829 m_playOrPauseCalledSinceLastStateChangedOrEosMessage = true;
830 qCDebug(qLcMediaPlayer) << "gst_play_play";
831 gst_play_play(m_gstPlay.get());
832 stateChanged(QMediaPlayer::PlayingState);
833}
834
836{
837 if (isCustomSource()) {
838 gstVideoOutput->setActive(true);
839 customPipeline.setState(GST_STATE_PAUSED);
840 stateChanged(QMediaPlayer::PausedState);
841 return;
842 }
843
844 if (m_hasPendingMedia) {
845 m_requestedPlaybackState = QMediaPlayer::PausedState;
846 return;
847 }
848
849 if (state() == QMediaPlayer::PausedState || !hasMedia()
850 || m_resourceErrorState != ResourceErrorState::NoError)
851 return;
852
853 gstVideoOutput->setActive(true);
854
855 m_callStopInDestructor = true;
856 m_playOrPauseCalledSinceLastStateChangedOrEosMessage = true;
857 qCDebug(qLcMediaPlayer) << "gst_play_pause";
858 gst_play_pause(m_gstPlay.get());
859
860 mediaStatusChanged(QMediaPlayer::BufferedMedia);
861 stateChanged(QMediaPlayer::PausedState);
862}
863
865{
866 if (isCustomSource()) {
867 customPipeline.setState(GST_STATE_READY);
868 stateChanged(QMediaPlayer::StoppedState);
869 gstVideoOutput->setActive(false);
870 return;
871 }
872
873 if (m_hasPendingMedia) {
874 m_requestedPlaybackState = QMediaPlayer::StoppedState;
875 return;
876 }
877
878 using namespace std::chrono_literals;
879 if (state() == QMediaPlayer::StoppedState) {
880 if (position() != 0) {
881 m_pendingSeek = 0ms;
882 positionChanged(0ms);
883 mediaStatusChanged(QMediaPlayer::LoadedMedia);
884 }
885 return;
886 }
887
888 gstVideoOutput->setActive(false);
889 m_expectedStoppedMessages += 1;
890 m_callStopInDestructor = false;
891 qCDebug(qLcMediaPlayer) << "gst_play_stop";
892 gst_play_stop(m_gstPlay.get());
893
894 stateChanged(QMediaPlayer::StoppedState);
895
896 mediaStatusChanged(QMediaPlayer::LoadedMedia);
897 positionChanged(0ms);
898}
899
901{
902 if (isCustomSource())
903 return customPipeline;
904
905 return m_playbin;
906}
907
909{
910 return true;
911}
912
914{
915 return true;
916}
917
920{
921 return PitchCompensationAvailability::AlwaysOn;
922}
923
925{
926 return m_url;
927}
928
929const QIODevice *QGstreamerMediaPlayer::mediaStream() const
930{
931 return m_stream;
932}
933
934void QGstreamerMediaPlayer::sourceSetupCallback([[maybe_unused]] GstElement *playbin,
935 GstElement *source, QGstreamerMediaPlayer *)
936{
937 // gst_play thread
938
939 const gchar *typeName = g_type_name_from_instance((GTypeInstance *)source);
940 qCDebug(qLcMediaPlayer) << "Setting up source:" << typeName;
941
942 if (typeName == std::string_view("GstRTSPSrc")) {
943 QGstElement s(source, QGstElement::NeedsRef);
944 int latency{40};
945 bool ok{false};
946 int v = qEnvironmentVariableIntValue("QT_MEDIA_RTSP_LATENCY", &ok);
947 if (ok)
948 latency = v;
949 qCDebug(qLcMediaPlayer) << " -> setting source latency to:" << latency << "ms";
950 s.set("latency", latency);
951
952 bool drop{true};
953 v = qEnvironmentVariableIntValue("QT_MEDIA_RTSP_DROP_ON_LATENCY", &ok);
954 if (ok && v == 0)
955 drop = false;
956 qCDebug(qLcMediaPlayer) << " -> setting drop-on-latency to:" << drop;
957 s.set("drop-on-latency", drop);
958
959 bool retrans{false};
960 v = qEnvironmentVariableIntValue("QT_MEDIA_RTSP_DO_RETRANSMISSION", &ok);
961 if (ok && v != 0)
962 retrans = true;
963 qCDebug(qLcMediaPlayer) << " -> setting do-retransmission to:" << retrans;
964 s.set("do-retransmission", retrans);
965 }
966}
967
968void QGstreamerMediaPlayer::setMedia(const QUrl &content, QIODevice *stream)
969{
970 if (customPipeline)
971 cleanupCustomPipeline();
972
973 m_resourceErrorState = ResourceErrorState::NoError;
974 m_url = content;
975 m_stream = stream;
976
977 // cancel any pending discovery continuations
978 m_discoveryHandler.cancel();
979 m_discoverFuture.cancel();
980
981 QUrl streamURL;
982 if (stream)
983 streamURL = qGstRegisterQIODevice(stream);
984
985 if (content.isEmpty() && !stream) {
986 mediaStatusChanged(QMediaPlayer::NoMedia);
987 resetStateForEmptyOrInvalidMedia();
988 return;
989 }
990
991 if (isCustomSource()) {
992 setMediaCustomSource(content);
993 } else {
994 mediaStatusChanged(QMediaPlayer::LoadingMedia);
995 m_hasPendingMedia = true;
996 m_requestedPlaybackState = std::nullopt;
997
998 const QUrl &playUrl = stream ? streamURL : content;
999 m_discoverFuture = discover(playUrl);
1000
1001 m_discoveryHandler =
1002 m_discoverFuture.then(this,[this, playUrl](const DiscoverResult &result) {
1003 handleDiscoverResult(result, playUrl);
1004 });
1005 }
1006}
1007
1008void QGstreamerMediaPlayer::setMediaCustomSource(const QUrl &content)
1009{
1010 using namespace Qt::Literals;
1011 using namespace std::chrono;
1012 using namespace std::chrono_literals;
1013
1014 {
1015 // FIXME: claim sinks
1016 // TODO: move ownership of sinks to gst_play after using them
1017 m_playbin.set("video-sink", QGstElement::createFromPipelineDescription("fakesink"));
1018 m_playbin.set("text-sink", QGstElement::createFromPipelineDescription("fakesink"));
1019 m_playbin.set("audio-sink", QGstElement::createFromPipelineDescription("fakesink"));
1020
1021 if (gstVideoOutput->gstreamerVideoSink()) {
1022 if (QGstElement sink = gstVideoOutput->gstreamerVideoSink()->gstSink())
1024 }
1025 }
1026
1027 customPipeline = QGstPipeline::create("customPipeline");
1028 customPipeline.installMessageFilter(this);
1029 positionUpdateTimer = std::make_unique<QTimer>();
1030
1031 QObject::connect(positionUpdateTimer.get(), &QTimer::timeout, this, [this] {
1032 Q_ASSERT(customPipeline);
1033 auto position = customPipeline.position();
1034
1035 positionChanged(round<milliseconds>(position));
1036 });
1037
1038 positionUpdateTimer->start(100ms);
1039
1040 QByteArray gstLaunchString =
1041 content.toString(QUrl::RemoveScheme | QUrl::PrettyDecoded).toLatin1();
1042 qCDebug(qLcMediaPlayer) << "generating" << gstLaunchString;
1044 if (!element) {
1045 emit error(QMediaPlayer::ResourceError, u"Could not create custom pipeline"_s);
1046 return;
1047 }
1048
1049 decoder = element;
1050 customPipeline.add(decoder);
1051
1052 QGstBin elementBin{
1053 qGstSafeCast<GstBin>(element.element()),
1054 QGstBin::NeedsRef,
1055 };
1056 if (elementBin) // bins are expected to provide unconnected src pads
1057 elementBin.addUnlinkedGhostPads(GstPadDirection::GST_PAD_SRC);
1058
1059 // for all other elements
1060 padAdded = decoder.onPadAdded<&QGstreamerMediaPlayer::decoderPadAddedCustomSource>(this);
1061 padRemoved = decoder.onPadRemoved<&QGstreamerMediaPlayer::decoderPadRemovedCustomSource>(this);
1062
1063 customPipeline.setStateSync(GstState::GST_STATE_PAUSED);
1064
1065 auto srcPadVisitor = [](GstElement *element, GstPad *pad, void *self) -> gboolean {
1066 reinterpret_cast<QGstreamerMediaPlayer *>(self)->decoderPadAddedCustomSource(
1067 QGstElement{ element, QGstElement::NeedsRef }, QGstPad{ pad, QGstPad::NeedsRef });
1068 return true;
1069 };
1070
1071 gst_element_foreach_pad(element.element(), srcPadVisitor, this);
1072
1073 mediaStatusChanged(QMediaPlayer::LoadedMedia);
1074
1075 customPipeline.dumpGraph("setMediaCustomPipeline");
1076}
1077
1078void QGstreamerMediaPlayer::cleanupCustomPipeline()
1079{
1080 customPipeline.setStateSync(GST_STATE_NULL);
1081 customPipeline.removeMessageFilter(this);
1082
1083 for (QGstElement &sink : customPipelineSinks)
1084 if (sink)
1085 customPipeline.remove(sink);
1086
1087 positionUpdateTimer = {};
1088 customPipeline = {};
1089}
1090
1091void QGstreamerMediaPlayer::setAudioOutput(QPlatformAudioOutput *output)
1092{
1093 if (isCustomSource()) {
1094 qWarning() << "QMediaPlayer::setAudioOutput not supported when using custom sources";
1095 return;
1096 }
1097
1098 if (gstAudioOutput == output)
1099 return;
1100
1101 auto *gstOutput = static_cast<QGstreamerAudioOutput *>(output);
1102 if (gstOutput)
1103 gstOutput->setAsync(true);
1104
1105 gstAudioOutput = static_cast<QGstreamerAudioOutput *>(output);
1106 if (gstAudioOutput)
1107 m_playbin.set("audio-sink", gstAudioOutput->gstElement());
1108 else
1109 m_playbin.set("audio-sink", QGstElement::createFromPipelineDescription("fakesink"));
1110 updateAudioTrackEnabled();
1111
1112 // FIXME: we need to have a gst_play API to change the sinks on the fly.
1113 // finishStateChange a hack to avoid assertion failures in gstreamer
1114 if (!qmediaplayerDestructorCalled)
1115 m_playbin.finishStateChange();
1116}
1117
1118QMediaMetaData QGstreamerMediaPlayer::metaData() const
1119{
1120 return m_metaData;
1121}
1122
1123void QGstreamerMediaPlayer::setVideoSink(QVideoSink *sink)
1124{
1125 // Disconnect previous sink
1127
1128 if (!sink)
1129 return;
1130
1131 // Connect pluggable sink to native sink
1132 auto pluggableSink = dynamic_cast<QGstreamerPluggableVideoSink *>(sink->platformVideoSink());
1133 Q_ASSERT(pluggableSink);
1134 m_gstVideoSink->connectPluggableVideoSink(pluggableSink);
1135}
1136
1137int QGstreamerMediaPlayer::trackCount(QPlatformMediaPlayer::TrackType type)
1138{
1139 QSpan<const QMediaMetaData> tracks = m_trackMetaData[type];
1140 return tracks.size();
1141}
1142
1143QMediaMetaData QGstreamerMediaPlayer::trackMetaData(QPlatformMediaPlayer::TrackType type, int index)
1144{
1145 QSpan<const QMediaMetaData> tracks = m_trackMetaData[type];
1146 if (index < tracks.size())
1147 return tracks[index];
1148 return {};
1149}
1150
1152{
1153 return m_activeTrack[type];
1154}
1155
1156void QGstreamerMediaPlayer::setActiveTrack(TrackType type, int index)
1157{
1158 if (m_activeTrack[type] == index)
1159 return;
1160
1161 int formerTrack = m_activeTrack[type];
1162 m_activeTrack[type] = index;
1163
1164 switch (type) {
1165 case TrackType::VideoStream: {
1166 if (index != -1)
1167 gst_play_set_video_track(m_gstPlay.get(), index);
1168 updateVideoTrackEnabled();
1169 updateNativeSizeOnVideoOutput();
1170 break;
1171 }
1172 case TrackType::AudioStream: {
1173 if (index != -1)
1174 gst_play_set_audio_track(m_gstPlay.get(), index);
1175 updateAudioTrackEnabled();
1176 break;
1177 }
1178 case TrackType::SubtitleStream: {
1179 if (index != -1)
1180 gst_play_set_subtitle_track(m_gstPlay.get(), index);
1181 gst_play_set_subtitle_track_enabled(m_gstPlay.get(), index != -1);
1182 break;
1183 }
1184 default:
1185 Q_UNREACHABLE();
1186 };
1187
1188 if (formerTrack != -1 && index != -1)
1189 // it can take several seconds for gstreamer to switch the track. so we seek to the current
1190 // position
1191 seekToCurrentPosition();
1192}
1193
1194QT_END_NAMESPACE
QGstStructureView at(int index) const
Definition qgst.cpp:621
void removeFromParent()
Definition qgst.cpp:1338
bool syncStateWithParent()
Definition qgst.cpp:1155
QGstPad sink() const
Definition qgst.cpp:1107
static QGstElement createFromPipelineDescription(const char *)
Definition qgst.cpp:1055
bool link(const QGstPad &sink) const
Definition qgst.cpp:902
QGstCaps queryCaps() const
Definition qgst.cpp:863
QGString streamId() const
Definition qgst.cpp:875
QGstElement gstElement() const
QUrl media() const override
bool processBusMessage(const QGstreamerMessage &message) override
PitchCompensationAvailability pitchCompensationAvailability() const override
void setPosition(qint64 pos) override
qreal playbackRate() const override
qint64 duration() const override
bool pitchCompensation() const override
void setPlaybackRate(qreal rate) override
QMediaTimeRange availablePlaybackRanges() const override
const QGstPipeline & pipeline() const
float bufferProgress() const override
void setActiveTrack(TrackType, int) override
void setAudioOutput(QPlatformAudioOutput *output) override
const QIODevice * mediaStream() const override
int activeTrack(TrackType) override
QMediaMetaData metaData() const override
bool canPlayQrc() const override
void connectPluggableVideoSink(QGstreamerPluggableVideoSink *pluggableSink)
void setVideoSink(QGstreamerRelayVideoSink *sink)
QGstreamerRelayVideoSink * gstreamerVideoSink() const
std::optional< QGstreamerMediaPlayer::TrackType > toTrackType(const QGstCaps &caps)
void setSeekAccurate(T *config, gboolean accurate)
QT_BEGIN_NAMESPACE Q_STATIC_LOGGING_CATEGORY(lcSynthesizedIterableAccess, "qt.iterable.synthesized", QtWarningMsg)
QGstPlayMessageAdaptor(const QGstreamerMessage &m)