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