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 error(QMediaPlayer::Error::ResourceError, u"Resource cannot be discovered"_s);
167 m_hasPendingMedia = false;
168 mediaStatusChanged(QMediaPlayer::InvalidMedia);
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 constexpr bool useNxpWorkaround = std::is_same_v<decltype(&gst_play_config_set_seek_accurate),
383 void (*)(GstPlay *, gboolean)>;
384
385 QUniqueGstStructureHandle config{
386 gst_play_get_config(m_gstPlay.get()),
387 };
388
389 if constexpr (useNxpWorkaround)
390 setSeekAccurate(m_gstPlay.get(), true);
391 else
392 setSeekAccurate(config.get(), true);
393
394 gst_play_set_config(m_gstPlay.get(), config.release());
395#else
396 QUniqueGstStructureHandle config{
397 gst_play_get_config(m_gstPlay.get()),
398 };
399 gst_play_config_set_seek_accurate(config.get(), true);
400 gst_play_set_config(m_gstPlay.get(), config.release());
401#endif
402
403 gstVideoOutput->setParent(this);
404
405 // NOTE: Creating a GStreamer video sink to be owned by the media player, any sink created by
406 // user would be a pluggable sink connected to this
407 m_gstVideoSink = new QGstreamerRelayVideoSink(this);
408 m_gstVideoSink->setRhi(qEnsureThreadLocalRhi());
409 gstVideoOutput->setVideoSink(m_gstVideoSink);
410
411 m_playbin.set("video-sink", gstVideoOutput->gstElement());
412 m_playbin.set("text-sink", gstVideoOutput->gstSubtitleElement());
413 m_playbin.set("audio-sink", QGstElement::createFromPipelineDescription("fakesink"));
414
415 m_gstPlayBus.installMessageFilter(this);
416
417 // we start without subtitles
418 gst_play_set_subtitle_track_enabled(m_gstPlay.get(), false);
419
420 sourceSetup = m_playbin.connect("source-setup", GCallback(sourceSetupCallback), this);
421
422 m_activeTrack.fill(-1);
423
424 // TODO: how to detect stalled media?
425}
426
428{
429 if (customPipeline)
430 cleanupCustomPipeline();
431
432 m_gstPlayBus.removeMessageFilter(static_cast<QGstreamerBusMessageFilter *>(this));
433 gst_bus_set_flushing(m_gstPlayBus.get(), TRUE);
434 gst_play_stop(m_gstPlay.get());
435
436 // NOTE: gst_play_stop is not sufficient, un-reffing m_gstPlay can deadlock
437 m_playbin.setStateSync(GST_STATE_NULL);
438
439 m_playbin.set("video-sink", QGstElement::createFromPipelineDescription("fakesink"));
440 m_playbin.set("text-sink", QGstElement::createFromPipelineDescription("fakesink"));
441 m_playbin.set("audio-sink", QGstElement::createFromPipelineDescription("fakesink"));
442}
443
444void QGstreamerMediaPlayer::updatePositionFromPipeline()
445{
446 using namespace std::chrono;
447
448 positionChanged(round<milliseconds>(nanoseconds{
449 gst_play_get_position(m_gstPlay.get()),
450 }));
451}
452
454{
455 if (isCustomSource()) {
456 constexpr bool traceBusMessages = true;
457 if (traceBusMessages)
458 qCDebug(qLcMediaPlayer) << "received bus message:" << message;
459
460 switch (message.type()) {
461 case GST_MESSAGE_WARNING:
462 qWarning() << "received bus message:" << message;
463 break;
464
465 case GST_MESSAGE_INFO:
466 qInfo() << "received bus message:" << message;
467 break;
468
469 case GST_MESSAGE_ERROR:
470 qWarning() << "received bus message:" << message;
471 customPipeline.dumpPipelineGraph("GST_MESSAGE_ERROR");
472 break;
473
474 case GST_MESSAGE_LATENCY:
475 customPipeline.recalculateLatency();
476 break;
477
478 default:
479 break;
480 }
481 return false;
482 }
483
484 switch (message.type()) {
485 case GST_MESSAGE_APPLICATION:
486 if (gst_play_is_play_message(message.message()))
487 return processBusMessageApplication(message);
488 return false;
489
490 default:
491 qCDebug(qLcMediaPlayer) << message;
492
493 return false;
494 }
495
496 return false;
497}
498
499bool QGstreamerMediaPlayer::processBusMessageApplication(const QGstreamerMessage &message)
500{
501 using namespace std::chrono;
502 GstPlayMessage type;
503 gst_play_message_parse_type(message.message(), &type);
504 qCDebug(qLcMediaPlayer) << QGstPlayMessageAdaptor{ message };
505
506 switch (type) {
507 case GST_PLAY_MESSAGE_URI_LOADED: {
508 mediaStatusChanged(QMediaPlayer::LoadedMedia);
509 return false;
510 }
511
512 case GST_PLAY_MESSAGE_POSITION_UPDATED: {
513 if (state() == QMediaPlayer::PlaybackState::PlayingState) {
514
515 constexpr bool usePayload = false;
516 if constexpr (usePayload) {
517 GstClockTime position;
518 gst_play_message_parse_position_updated(message.message(), &position);
519 positionChanged(round<milliseconds>(nanoseconds{ position }));
520 } else {
521 GstClockTime position = gst_play_get_position(m_gstPlay.get());
522 positionChanged(round<milliseconds>(nanoseconds{ position }));
523 }
524 }
525 return false;
526 }
527 case GST_PLAY_MESSAGE_DURATION_CHANGED: {
528 GstClockTime duration;
529 gst_play_message_parse_duration_updated(message.message(), &duration);
530 milliseconds durationInMs = round<milliseconds>(nanoseconds{ duration });
531 durationChanged(durationInMs);
532
533 m_metaData.insert(QMediaMetaData::Duration, int(durationInMs.count()));
534 metaDataChanged();
535
536 return false;
537 }
538 case GST_PLAY_MESSAGE_BUFFERING: {
539 guint percent;
540 gst_play_message_parse_buffering_percent(message.message(), &percent);
541 updateBufferProgress(percent * 0.01f);
542 return false;
543 }
544 case GST_PLAY_MESSAGE_STATE_CHANGED: {
545 GstPlayState state;
546 gst_play_message_parse_state_changed(message.message(), &state);
547
548 switch (state) {
549 case GstPlayState::GST_PLAY_STATE_STOPPED:
550 if (stateChangeToSkip) {
551 qCDebug(qLcMediaPlayer) << " skipping StoppedState transition";
552
553 stateChangeToSkip -= 1;
554 return false;
555 }
556 stateChanged(QMediaPlayer::StoppedState);
557 updateBufferProgress(0);
558 return false;
559
560 case GstPlayState::GST_PLAY_STATE_PAUSED:
561 stateChanged(QMediaPlayer::PausedState);
562 mediaStatusChanged(QMediaPlayer::BufferedMedia);
563 gstVideoOutput->setActive(true);
564 updateBufferProgress(1);
565 return false;
566 case GstPlayState::GST_PLAY_STATE_BUFFERING:
567 mediaStatusChanged(QMediaPlayer::BufferingMedia);
568 return false;
569 case GstPlayState::GST_PLAY_STATE_PLAYING:
570 stateChanged(QMediaPlayer::PlayingState);
571 mediaStatusChanged(QMediaPlayer::BufferedMedia);
572 gstVideoOutput->setActive(true);
573 updateBufferProgress(1);
574
575 return false;
576 default:
577 return false;
578 }
579 }
580 case GST_PLAY_MESSAGE_MEDIA_INFO_UPDATED: {
581 using namespace QGstPlaySupport;
582
583 QUniqueGstPlayMediaInfoHandle info{};
584 gst_play_message_parse_media_info_updated(message.message(), &info);
585
586 seekableChanged(gst_play_media_info_is_seekable(info.get()));
587
588 const gchar *title = gst_play_media_info_get_title(info.get());
589 m_metaData.insert(QMediaMetaData::Title, QString::fromUtf8(title));
590
591 metaDataChanged();
592 tracksChanged();
593
594 return false;
595 }
596 case GST_PLAY_MESSAGE_END_OF_STREAM: {
597 if (doLoop()) {
598 positionChanged(m_duration);
599 qCDebug(qLcMediaPlayer) << "EOS: restarting loop";
600 gst_play_play(m_gstPlay.get());
601 positionChanged(0ms);
602
603 // we will still get a GST_PLAY_MESSAGE_STATE_CHANGED message, which we will just ignore
604 // for now
605 stateChangeToSkip += 1;
606 } else {
607 qCDebug(qLcMediaPlayer) << "EOS: done";
608 positionChanged(m_duration);
609 mediaStatusChanged(QMediaPlayer::EndOfMedia);
610 stateChanged(QMediaPlayer::StoppedState);
611 gstVideoOutput->setActive(false);
612 }
613
614 return false;
615 }
616 case GST_PLAY_MESSAGE_ERROR:
617 case GST_PLAY_MESSAGE_WARNING:
618 case GST_PLAY_MESSAGE_VIDEO_DIMENSIONS_CHANGED:
619 case GST_PLAY_MESSAGE_VOLUME_CHANGED:
620 case GST_PLAY_MESSAGE_MUTE_CHANGED:
621 case GST_PLAY_MESSAGE_SEEK_DONE:
622 return false;
623
624 default:
625 Q_UNREACHABLE_RETURN(false);
626 }
627}
628
630{
631 return m_duration.count();
632}
633
634bool QGstreamerMediaPlayer::hasMedia() const
635{
636 return !m_url.isEmpty() || m_stream;
637}
638
639bool QGstreamerMediaPlayer::hasValidMedia() const
640{
641 if (!hasMedia())
642 return false;
643
644 switch (mediaStatus()) {
645 case QMediaPlayer::MediaStatus::NoMedia:
646 case QMediaPlayer::MediaStatus::InvalidMedia:
647 return false;
648
649 default:
650 return true;
651 }
652}
653
655{
656 return m_bufferProgress;
657}
658
660{
661 return QMediaTimeRange();
662}
663
665{
666 return gst_play_get_rate(m_gstPlay.get());
667}
668
670{
671 if (isCustomSource()) {
672 static std::once_flag flag;
673 std::call_once(flag, [] {
674 // CAVEAT: unsynchronised with pipeline state. Potentially prone to race conditions
675 qWarning()
676 << "setPlaybackRate with custom gstreamer pipelines can cause pipeline hangs. "
677 "Use with care";
678 });
679
680 customPipeline.setPlaybackRate(rate);
681 return;
682 }
683
684 if (rate == playbackRate())
685 return;
686
687 qCDebug(qLcMediaPlayer) << "gst_play_set_rate" << rate;
688 gst_play_set_rate(m_gstPlay.get(), rate);
689 playbackRateChanged(rate);
690}
691
693{
694 std::chrono::milliseconds posInMs{ pos };
695
696 setPosition(posInMs);
697}
698
699void QGstreamerMediaPlayer::setPosition(std::chrono::milliseconds pos)
700{
701 using namespace std::chrono;
702
703 if (isCustomSource()) {
704 static std::once_flag flag;
705 std::call_once(flag, [] {
706 // CAVEAT: unsynchronised with pipeline state. Potentially prone to race conditions
707 qWarning() << "setPosition with custom gstreamer pipelines can cause pipeline hangs. "
708 "Use with care";
709 });
710
711 customPipeline.setPosition(pos);
712 return;
713 }
714
715 if (m_hasPendingMedia) {
716 return;
717 }
718
719 qCDebug(qLcMediaPlayer) << "gst_play_seek" << pos;
720 gst_play_seek(m_gstPlay.get(), nanoseconds(pos).count());
721
722 if (mediaStatus() == QMediaPlayer::EndOfMedia)
723 mediaStatusChanged(QMediaPlayer::LoadedMedia);
724 positionChanged(pos);
725}
726
728{
729 if (isCustomSource()) {
730 gstVideoOutput->setActive(true);
731 customPipeline.setState(GST_STATE_PLAYING);
732 stateChanged(QMediaPlayer::PlayingState);
733 return;
734 }
735
736 if (m_hasPendingMedia) {
737 // Async media loading in progress via QGstDiscoverer, m_discoveryHandler will fulfill the
738 // last requested playback state later.
739 m_requestedPlaybackState = QMediaPlayer::PlayingState;
740 return;
741 }
742
743 QMediaPlayer::PlaybackState currentState = state();
744 if (currentState == QMediaPlayer::PlayingState || !hasValidMedia())
745 return;
746
747 if (currentState != QMediaPlayer::PausedState)
748 resetCurrentLoop();
749
750 if (mediaStatus() == QMediaPlayer::EndOfMedia) {
751 positionChanged(0);
752 mediaStatusChanged(QMediaPlayer::LoadedMedia);
753 }
754
755 if (m_pendingSeek) {
756 gst_play_seek(m_gstPlay.get(), m_pendingSeek->count());
757 m_pendingSeek = std::nullopt;
758 }
759
760 qCDebug(qLcMediaPlayer) << "gst_play_play";
761 gstVideoOutput->setActive(true);
762 gst_play_play(m_gstPlay.get());
763 stateChanged(QMediaPlayer::PlayingState);
764}
765
767{
768 if (isCustomSource()) {
769 gstVideoOutput->setActive(true);
770 customPipeline.setState(GST_STATE_PAUSED);
771 stateChanged(QMediaPlayer::PausedState);
772 return;
773 }
774
775 if (m_hasPendingMedia) {
776 m_requestedPlaybackState = QMediaPlayer::PausedState;
777 return;
778 }
779
780 if (state() == QMediaPlayer::PausedState || !hasMedia()
781 || m_resourceErrorState != ResourceErrorState::NoError)
782 return;
783
784 gstVideoOutput->setActive(true);
785
786 qCDebug(qLcMediaPlayer) << "gst_play_pause";
787 gst_play_pause(m_gstPlay.get());
788
789 mediaStatusChanged(QMediaPlayer::BufferedMedia);
790 stateChanged(QMediaPlayer::PausedState);
791}
792
794{
795 if (isCustomSource()) {
796 customPipeline.setState(GST_STATE_READY);
797 stateChanged(QMediaPlayer::StoppedState);
798 gstVideoOutput->setActive(false);
799 return;
800 }
801
802 if (m_hasPendingMedia) {
803 m_requestedPlaybackState = QMediaPlayer::StoppedState;
804 return;
805 }
806
807 using namespace std::chrono_literals;
808 if (state() == QMediaPlayer::StoppedState) {
809 if (position() != 0) {
810 m_pendingSeek = 0ms;
811 positionChanged(0ms);
812 mediaStatusChanged(QMediaPlayer::LoadedMedia);
813 }
814 return;
815 }
816
817 qCDebug(qLcMediaPlayer) << "gst_play_stop";
818 gstVideoOutput->setActive(false);
819 gst_play_stop(m_gstPlay.get());
820
821 stateChanged(QMediaPlayer::StoppedState);
822
823 mediaStatusChanged(QMediaPlayer::LoadedMedia);
824 positionChanged(0ms);
825}
826
828{
829 if (isCustomSource())
830 return customPipeline;
831
832 return m_playbin;
833}
834
836{
837 return true;
838}
839
841{
842 return true;
843}
844
847{
848 return PitchCompensationAvailability::AlwaysOn;
849}
850
852{
853 return m_url;
854}
855
856const QIODevice *QGstreamerMediaPlayer::mediaStream() const
857{
858 return m_stream;
859}
860
861void QGstreamerMediaPlayer::sourceSetupCallback([[maybe_unused]] GstElement *playbin,
862 GstElement *source, QGstreamerMediaPlayer *)
863{
864 // gst_play thread
865
866 const gchar *typeName = g_type_name_from_instance((GTypeInstance *)source);
867 qCDebug(qLcMediaPlayer) << "Setting up source:" << typeName;
868
869 if (typeName == std::string_view("GstRTSPSrc")) {
870 QGstElement s(source, QGstElement::NeedsRef);
871 int latency{40};
872 bool ok{false};
873 int v = qEnvironmentVariableIntValue("QT_MEDIA_RTSP_LATENCY", &ok);
874 if (ok)
875 latency = v;
876 qCDebug(qLcMediaPlayer) << " -> setting source latency to:" << latency << "ms";
877 s.set("latency", latency);
878
879 bool drop{true};
880 v = qEnvironmentVariableIntValue("QT_MEDIA_RTSP_DROP_ON_LATENCY", &ok);
881 if (ok && v == 0)
882 drop = false;
883 qCDebug(qLcMediaPlayer) << " -> setting drop-on-latency to:" << drop;
884 s.set("drop-on-latency", drop);
885
886 bool retrans{false};
887 v = qEnvironmentVariableIntValue("QT_MEDIA_RTSP_DO_RETRANSMISSION", &ok);
888 if (ok && v != 0)
889 retrans = true;
890 qCDebug(qLcMediaPlayer) << " -> setting do-retransmission to:" << retrans;
891 s.set("do-retransmission", retrans);
892 }
893}
894
895void QGstreamerMediaPlayer::setMedia(const QUrl &content, QIODevice *stream)
896{
897 if (customPipeline)
898 cleanupCustomPipeline();
899
900 m_resourceErrorState = ResourceErrorState::NoError;
901 m_url = content;
902 m_stream = stream;
903
904 // cancel any pending discovery continuations
905 m_discoveryHandler.cancel();
906 m_discoverFuture.cancel();
907
908 QUrl streamURL;
909 if (stream)
910 streamURL = qGstRegisterQIODevice(stream);
911
912 if (content.isEmpty() && !stream) {
913 mediaStatusChanged(QMediaPlayer::NoMedia);
914 resetStateForEmptyOrInvalidMedia();
915 return;
916 }
917
918 if (isCustomSource()) {
919 setMediaCustomSource(content);
920 } else {
921 mediaStatusChanged(QMediaPlayer::LoadingMedia);
922 m_hasPendingMedia = true;
923 m_requestedPlaybackState = std::nullopt;
924
925 const QUrl &playUrl = stream ? streamURL : content;
926 m_discoverFuture = discover(playUrl);
927
928 m_discoveryHandler =
929 m_discoverFuture.then(this,[this, playUrl](const DiscoverResult &result) {
930 handleDiscoverResult(result, playUrl);
931 });
932 }
933}
934
935void QGstreamerMediaPlayer::setMediaCustomSource(const QUrl &content)
936{
937 using namespace Qt::Literals;
938 using namespace std::chrono;
939 using namespace std::chrono_literals;
940
941 {
942 // FIXME: claim sinks
943 // TODO: move ownership of sinks to gst_play after using them
944 m_playbin.set("video-sink", QGstElement::createFromPipelineDescription("fakesink"));
945 m_playbin.set("text-sink", QGstElement::createFromPipelineDescription("fakesink"));
946 m_playbin.set("audio-sink", QGstElement::createFromPipelineDescription("fakesink"));
947
948 if (gstVideoOutput->gstreamerVideoSink()) {
949 if (QGstElement sink = gstVideoOutput->gstreamerVideoSink()->gstSink())
951 }
952 }
953
954 customPipeline = QGstPipeline::create("customPipeline");
955 customPipeline.installMessageFilter(this);
956 positionUpdateTimer = std::make_unique<QTimer>();
957
958 QObject::connect(positionUpdateTimer.get(), &QTimer::timeout, this, [this] {
959 Q_ASSERT(customPipeline);
960 auto position = customPipeline.position();
961
962 positionChanged(round<milliseconds>(position));
963 });
964
965 positionUpdateTimer->start(100ms);
966
967 QByteArray gstLaunchString =
968 content.toString(QUrl::RemoveScheme | QUrl::PrettyDecoded).toLatin1();
969 qCDebug(qLcMediaPlayer) << "generating" << gstLaunchString;
971 if (!element) {
972 emit error(QMediaPlayer::ResourceError, u"Could not create custom pipeline"_s);
973 return;
974 }
975
976 decoder = element;
977 customPipeline.add(decoder);
978
979 QGstBin elementBin{
980 qGstSafeCast<GstBin>(element.element()),
981 QGstBin::NeedsRef,
982 };
983 if (elementBin) // bins are expected to provide unconnected src pads
984 elementBin.addUnlinkedGhostPads(GstPadDirection::GST_PAD_SRC);
985
986 // for all other elements
987 padAdded = decoder.onPadAdded<&QGstreamerMediaPlayer::decoderPadAddedCustomSource>(this);
988 padRemoved = decoder.onPadRemoved<&QGstreamerMediaPlayer::decoderPadRemovedCustomSource>(this);
989
990 customPipeline.setStateSync(GstState::GST_STATE_PAUSED);
991
992 auto srcPadVisitor = [](GstElement *element, GstPad *pad, void *self) -> gboolean {
993 reinterpret_cast<QGstreamerMediaPlayer *>(self)->decoderPadAddedCustomSource(
994 QGstElement{ element, QGstElement::NeedsRef }, QGstPad{ pad, QGstPad::NeedsRef });
995 return true;
996 };
997
998 gst_element_foreach_pad(element.element(), srcPadVisitor, this);
999
1000 mediaStatusChanged(QMediaPlayer::LoadedMedia);
1001
1002 customPipeline.dumpGraph("setMediaCustomPipeline");
1003}
1004
1005void QGstreamerMediaPlayer::cleanupCustomPipeline()
1006{
1007 customPipeline.setStateSync(GST_STATE_NULL);
1008 customPipeline.removeMessageFilter(this);
1009
1010 for (QGstElement &sink : customPipelineSinks)
1011 if (sink)
1012 customPipeline.remove(sink);
1013
1014 positionUpdateTimer = {};
1015 customPipeline = {};
1016}
1017
1018void QGstreamerMediaPlayer::setAudioOutput(QPlatformAudioOutput *output)
1019{
1020 if (isCustomSource()) {
1021 qWarning() << "QMediaPlayer::setAudioOutput not supported when using custom sources";
1022 return;
1023 }
1024
1025 if (gstAudioOutput == output)
1026 return;
1027
1028 auto *gstOutput = static_cast<QGstreamerAudioOutput *>(output);
1029 if (gstOutput)
1030 gstOutput->setAsync(true);
1031
1032 gstAudioOutput = static_cast<QGstreamerAudioOutput *>(output);
1033 if (gstAudioOutput)
1034 m_playbin.set("audio-sink", gstAudioOutput->gstElement());
1035 else
1036 m_playbin.set("audio-sink", QGstElement::createFromPipelineDescription("fakesink"));
1037 updateAudioTrackEnabled();
1038
1039 // FIXME: we need to have a gst_play API to change the sinks on the fly.
1040 // finishStateChange a hack to avoid assertion failures in gstreamer
1041 if (!qmediaplayerDestructorCalled)
1042 m_playbin.finishStateChange();
1043}
1044
1045QMediaMetaData QGstreamerMediaPlayer::metaData() const
1046{
1047 return m_metaData;
1048}
1049
1050void QGstreamerMediaPlayer::setVideoSink(QVideoSink *sink)
1051{
1052 // Disconnect previous sink
1054
1055 if (!sink)
1056 return;
1057
1058 // Connect pluggable sink to native sink
1059 auto pluggableSink = dynamic_cast<QGstreamerPluggableVideoSink *>(sink->platformVideoSink());
1060 Q_ASSERT(pluggableSink);
1061 m_gstVideoSink->connectPluggableVideoSink(pluggableSink);
1062}
1063
1064int QGstreamerMediaPlayer::trackCount(QPlatformMediaPlayer::TrackType type)
1065{
1066 QSpan<const QMediaMetaData> tracks = m_trackMetaData[type];
1067 return tracks.size();
1068}
1069
1070QMediaMetaData QGstreamerMediaPlayer::trackMetaData(QPlatformMediaPlayer::TrackType type, int index)
1071{
1072 QSpan<const QMediaMetaData> tracks = m_trackMetaData[type];
1073 if (index < tracks.size())
1074 return tracks[index];
1075 return {};
1076}
1077
1079{
1080 return m_activeTrack[type];
1081}
1082
1083void QGstreamerMediaPlayer::setActiveTrack(TrackType type, int index)
1084{
1085 if (m_activeTrack[type] == index)
1086 return;
1087
1088 int formerTrack = m_activeTrack[type];
1089 m_activeTrack[type] = index;
1090
1091 switch (type) {
1092 case TrackType::VideoStream: {
1093 if (index != -1)
1094 gst_play_set_video_track(m_gstPlay.get(), index);
1095 updateVideoTrackEnabled();
1096 updateNativeSizeOnVideoOutput();
1097 break;
1098 }
1099 case TrackType::AudioStream: {
1100 if (index != -1)
1101 gst_play_set_audio_track(m_gstPlay.get(), index);
1102 updateAudioTrackEnabled();
1103 break;
1104 }
1105 case TrackType::SubtitleStream: {
1106 if (index != -1)
1107 gst_play_set_subtitle_track(m_gstPlay.get(), index);
1108 gst_play_set_subtitle_track_enabled(m_gstPlay.get(), index != -1);
1109 break;
1110 }
1111 default:
1112 Q_UNREACHABLE();
1113 };
1114
1115 if (formerTrack != -1 && index != -1)
1116 // it can take several seconds for gstreamer to switch the track. so we seek to the current
1117 // position
1118 seekToCurrentPosition();
1119}
1120
1121QT_END_NAMESPACE
QGstStructureView at(int index) const
Definition qgst.cpp:554
void removeFromParent()
Definition qgst.cpp:1261
bool syncStateWithParent()
Definition qgst.cpp:1078
QGstPad sink() const
Definition qgst.cpp:1030
static QGstElement createFromPipelineDescription(const char *)
Definition qgst.cpp:988
bool link(const QGstPad &sink) const
Definition qgst.cpp:835
QGstCaps queryCaps() const
Definition qgst.cpp:796
QGString streamId() const
Definition qgst.cpp:808
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)