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