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