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