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