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
qohosmediaplayer.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 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
5
7#include "common/qohosvideooutput_p.h"
8#include <QtMultimedia/private/qplatformvideosink_p.h>
9
10#include <private/qplatformaudiooutput_p.h>
11
12#include <QtMultimedia/qaudiooutput.h>
13#include <QtMultimedia/qmediaplayer.h>
14#include <QtMultimedia/qmediametadata.h>
15#include <QtMultimedia/qmediatimerange.h>
16#include <QtMultimedia/qvideosink.h>
17
18#include <QtCore/qfile.h>
19#include <QtCore/qtimer.h>
20#include <QtCore/qfileinfo.h>
21#include <QtCore/qtemporaryfile.h>
22#include <QtCore/qloggingcategory.h>
23#include <QtCore/qmetaobject.h>
24#include <QtCore/qpointer.h>
25
26#include <multimedia/player_framework/native_averrors.h>
27
29
30static constexpr int surfaceWaitTimeoutMs = 500;
31
32namespace {
33
34QMediaPlayer::MediaStatus mediaStatusFor(AVPlayerState state, bool atEnd)
35{
36 if (atEnd)
37 return QMediaPlayer::EndOfMedia;
38
39 switch (state) {
40 case AV_IDLE:
41 return QMediaPlayer::NoMedia;
42 case AV_INITIALIZED:
43 return QMediaPlayer::LoadingMedia;
44 case AV_PREPARED:
45 case AV_STOPPED:
46 return QMediaPlayer::LoadedMedia;
47 case AV_PLAYING:
48 case AV_PAUSED:
49 return QMediaPlayer::BufferedMedia;
50 case AV_COMPLETED:
51 return QMediaPlayer::EndOfMedia;
52 case AV_RELEASED:
53 case AV_ERROR:
54 return QMediaPlayer::InvalidMedia;
55 }
56 return QMediaPlayer::NoMedia;
57}
58
59QMediaPlayer::PlaybackState playbackStateFor(AVPlayerState state)
60{
61 switch (state) {
62 case AV_PLAYING:
63 return QMediaPlayer::PlayingState;
64 case AV_PAUSED:
65 return QMediaPlayer::PausedState;
66 case AV_IDLE:
67 case AV_INITIALIZED:
68 case AV_PREPARED:
69 case AV_STOPPED:
70 case AV_COMPLETED:
71 case AV_RELEASED:
72 case AV_ERROR:
73 break;
74 }
75 return QMediaPlayer::StoppedState;
76}
77
78AVPlaybackSpeed playbackSpeedFor(qreal rate)
79{
80 // Map onto the closest discrete OH speed enum (SetPlaybackRate is API 20+).
81 if (rate <= 0.1875)
82 return AV_SPEED_FORWARD_0_125_X;
83 if (rate <= 0.375)
84 return AV_SPEED_FORWARD_0_25_X;
85 if (rate <= 0.625)
86 return AV_SPEED_FORWARD_0_50_X;
87 if (rate <= 0.875)
88 return AV_SPEED_FORWARD_0_75_X;
89 if (rate < 1.125)
90 return AV_SPEED_FORWARD_1_00_X;
91 if (rate < 1.375)
92 return AV_SPEED_FORWARD_1_25_X;
93 if (rate < 1.625)
94 return AV_SPEED_FORWARD_1_50_X;
95 if (rate < 1.875)
96 return AV_SPEED_FORWARD_1_75_X;
97 if (rate < 2.5)
98 return AV_SPEED_FORWARD_2_00_X;
99 return AV_SPEED_FORWARD_3_00_X;
100}
101
102} // namespace
103
104QOhosMediaPlayer::QOhosMediaPlayer(QMediaPlayer *parent)
105 : QObject(parent), QPlatformMediaPlayer(parent)
106{
107}
108
110{
111 releasePlayer();
112}
113
115{
116 return m_duration;
117}
118
120{
121 return m_position.load();
122}
123
124void QOhosMediaPlayer::setPosition(qint64 position)
125{
126 if (!m_player)
127 return;
128 OH_AVPlayer_Seek(m_player, static_cast<int32_t>(position), AV_SEEK_NEXT_SYNC);
129}
130
132{
133 return m_bufferProgress;
134}
135
137{
138 return m_seekable;
139}
140
142{
143 if (m_duration <= 0)
144 return {};
145 return QMediaTimeRange{ 0, m_duration };
146}
147
149{
150 return m_playbackRate;
151}
152
154{
155 if (qFuzzyCompare(m_playbackRate, rate))
156 return;
157 m_playbackRate = rate;
158 if (m_player)
159 OH_AVPlayer_SetPlaybackSpeed(m_player, playbackSpeedFor(rate));
160 playbackRateChanged(rate);
161}
162
164{
165 return m_media;
166}
167
169{
170 return m_stream;
171}
172
173void QOhosMediaPlayer::setMedia(const QUrl &media, QIODevice *stream)
174{
175 m_media = media;
176 m_stream = stream;
177
178 if (m_player)
179 OH_AVPlayer_Reset(m_player);
180 clearSource();
181
182 if (media.isEmpty()) {
183 m_duration = 0;
184 m_position.store(0);
185 m_pendingSetMedia = false;
186 mediaStatusChanged(QMediaPlayer::NoMedia);
187 stateChanged(QMediaPlayer::StoppedState);
188 return;
189 }
190
191 // Reject obviously bad file-like sources synchronously so the test
192 // QMediaPlayer-style "invalid file" cases see ResourceError immediately
193 // instead of waiting forever on the deferred surface. QMediaPlayer's
194 // observable contract is "Loading -> Invalid", so emit Loading first even
195 // when the failure is immediate.
196 const QString scheme = media.scheme();
197 if (media.isLocalFile() || scheme.isEmpty() || scheme == QLatin1String("qrc")) {
198 QString openPath;
199 if (media.isLocalFile())
200 openPath = media.toLocalFile();
201 else if (scheme == QLatin1String("qrc"))
202 openPath = QLatin1Char(':') + media.path();
203 else
204 openPath = media.toString();
205 QFile probe(openPath);
206 if (!probe.open(QIODevice::ReadOnly)) {
207 mediaStatusChanged(QMediaPlayer::LoadingMedia);
208 setInvalidMediaWithError(QMediaPlayer::ResourceError,
209 QStringLiteral("Cannot open '%1'").arg(openPath));
210 return;
211 }
212 }
213
214 m_pendingSetMedia = false;
215 m_sourceApplied = false;
216 m_surfaceWaitStarted = false;
217 // Apply on the next event-loop cycle so a videoOutput assigned right after
218 // the source is in place before we pick a prepare path.
219 QMetaObject::invokeMethod(this, &QOhosMediaPlayer::tryApplyPendingSource,
220 Qt::QueuedConnection);
221}
222
223void QOhosMediaPlayer::tryApplyPendingSource()
224{
225 if (m_sourceApplied || m_media.isEmpty())
226 return;
227
228 // The surface can only be attached between SetSource and Prepare, so wait
229 // for it. A sink with no renderer never gets an RHI: give up and prepare
230 // without video rather than stall.
231 auto *rhiSink = m_videoSink ? qobject_cast<QRhiVideoSink *>(m_videoSink->platformVideoSink())
232 : nullptr;
233 const bool hasRhi = rhiSink && rhiSink->withRhi([](QRhi *rhi) {
234 return rhi != nullptr;
235 });
236 if (m_videoOutput && m_videoSink && !hasRhi) {
237 if (!m_surfaceWaitStarted) {
238 m_surfaceWaitStarted = true;
239 QTimer::singleShot(surfaceWaitTimeoutMs, this, [this] {
240 if (!m_sourceApplied && m_pendingSetMedia && !m_media.isEmpty()) {
241 m_pendingSetMedia = false;
242 m_sourceApplied = true;
243 applyPendingSource();
244 }
245 });
246 }
247 m_pendingSetMedia = true;
248 mediaStatusChanged(QMediaPlayer::LoadingMedia);
249 return;
250 }
251
252 m_sourceApplied = true;
253 applyPendingSource();
254}
255
256void QOhosMediaPlayer::applyPendingSource()
257{
258 if (!ensurePlayer()) {
259 setInvalidMediaWithError(QMediaPlayer::ResourceError,
260 QStringLiteral("Failed to create OH_AVPlayer"));
261 return;
262 }
263
264 OH_AVPlayer_SetAudioRendererInfo(m_player, AUDIOSTREAM_USAGE_MOVIE);
265
266 m_videoSurfaceAttached = false;
267
268 // Surface the LoadingMedia status before we hand the source to OH_AVPlayer
269 // so any synchronous SetURLSource/SetFDSource rejection still produces the
270 // observable Loading -> Invalid transition QMediaPlayer guarantees.
271 mediaStatusChanged(QMediaPlayer::LoadingMedia);
272
273 OH_AVErrCode result = AV_ERR_OK;
274 const QString scheme = m_media.scheme();
275 const bool isQrc = scheme == QLatin1String("qrc");
276 const bool isFileLike = m_media.isLocalFile() || scheme.isEmpty() || isQrc;
277 if (isFileLike) {
278 QString openPath;
279 if (m_media.isLocalFile()) {
280 openPath = m_media.toLocalFile();
281 } else if (isQrc) {
282 // QFile understands ":/path" for embedded resources.
283 openPath = QLatin1Char(':') + m_media.path();
284 } else {
285 // Bare relative paths.
286 openPath = m_media.toString();
287 }
288 auto file = std::make_unique<QFile>(openPath);
289 if (!file->open(QIODevice::ReadOnly)) {
290 setInvalidMediaWithError(QMediaPlayer::ResourceError,
291 QStringLiteral("Cannot open '%1'").arg(openPath));
292 return;
293 }
294 const int fd = file->handle();
295 const qint64 size = file->size();
296 if (fd < 0) {
297 // Resource-backed QFile is mapped, not file-descriptor-backed —
298 // AVPlayer needs a real fd, so spool to a temp file before handing
299 // it off.
300 auto temp = std::make_unique<QTemporaryFile>();
301 if (!temp->open() || temp->write(file->readAll()) < 0) {
302 setInvalidMediaWithError(QMediaPlayer::ResourceError,
303 QStringLiteral("Cannot stage '%1'").arg(openPath));
304 return;
305 }
306 temp->flush();
307 temp->seek(0);
308 const int tempFd = temp->handle();
309 const qint64 tempSize = temp->size();
310 result = OH_AVPlayer_SetFDSource(m_player, tempFd, 0, tempSize);
311 m_sourceFile = std::move(temp);
312 } else {
313 result = OH_AVPlayer_SetFDSource(m_player, fd, 0, size);
314 m_sourceFile = std::move(file);
315 }
316 } else {
317 const QByteArray url = m_media.toString(QUrl::FullyEncoded).toUtf8();
318 result = OH_AVPlayer_SetURLSource(m_player, url.constData());
319 }
320
321 if (result != AV_ERR_OK) {
322 qCWarning(qLcOhosMediaPlugin) << "Source rejected, error" << int(result);
323 setInvalidMediaWithError(QMediaPlayer::ResourceError,
324 QStringLiteral("OH_AVPlayer rejected the source"));
325 clearSource();
326 return;
327 }
328
329 if (m_videoOutput) {
330 if (auto *window = m_videoOutput->nativeWindow()) {
331 if (OH_AVPlayer_SetVideoSurface(m_player, window) == AV_ERR_OK)
332 m_videoSurfaceAttached = true;
333 else
334 qCWarning(qLcOhosMediaPlugin) << "OH_AVPlayer_SetVideoSurface failed";
335 }
336 }
337
338 result = OH_AVPlayer_Prepare(m_player);
339 if (result != AV_ERR_OK) {
340 qCWarning(qLcOhosMediaPlugin) << "Prepare failed, error" << int(result);
341 setInvalidMediaWithError(QMediaPlayer::ResourceError,
342 QStringLiteral("OH_AVPlayer_Prepare failed"));
343 clearSource();
344 }
345}
346
348{
349 if (!m_player) {
350 m_pendingPlay = !m_media.isEmpty();
351 return;
352 }
353
354 if (m_avState == AV_PREPARED || m_avState == AV_PAUSED || m_avState == AV_COMPLETED
355 || m_avState == AV_STOPPED) {
356 m_pendingPlay = false;
357 OH_AVPlayer_Play(m_player);
358 } else {
359 // Prepare hasn't finished yet — defer play to AV_INFO_TYPE_STATE_CHANGE
360 m_pendingPlay = true;
361 }
362}
363
365{
366 m_pendingPlay = false;
367 if (m_player && m_avState == AV_PLAYING)
368 OH_AVPlayer_Pause(m_player);
369}
370
372{
373 m_pendingPlay = false;
374 if (!m_player)
375 return;
376 if (m_avState == AV_PLAYING || m_avState == AV_PAUSED || m_avState == AV_PREPARED
377 || m_avState == AV_COMPLETED)
378 OH_AVPlayer_Stop(m_player);
379}
380
381void QOhosMediaPlayer::setAudioOutput(QPlatformAudioOutput *output)
382{
383 m_audioOutput = output;
384 applyVolume();
385}
386
387void QOhosMediaPlayer::setVideoSink(QVideoSink *sink)
388{
389 m_videoSink = sink;
390
391 if (!sink) {
392 m_videoOutput.reset();
393 return;
394 }
395
396 if (!m_videoOutput) {
397 m_videoOutput = std::make_unique<QOhosVideoOutput>(
398 sink, QOhosVideoOutput::ContentSource::MediaPlayer, this);
399 connect(m_videoOutput.get(), &QOhosVideoOutput::surfaceReady, this,
400 &QOhosMediaPlayer::onVideoSurfaceReady);
401 }
402}
403
404int QOhosMediaPlayer::trackCount(TrackType type)
405{
406 // OH_AVPlayer doesn't enumerate per-stream metadata up front; expose the
407 // detected presence of audio/video as a single default track so QMediaPlayer
408 // consumers can ask for activeTrack(0) and get a sensible answer.
409 switch (type) {
410 case TrackType::VideoStream:
411 return m_hasVideoTrack ? 1 : 0;
412 case TrackType::AudioStream:
413 return m_hasAudioTrack ? 1 : 0;
414 case TrackType::SubtitleStream:
415 break;
416 }
417 return 0;
418}
419
420QMediaMetaData QOhosMediaPlayer::trackMetaData(TrackType type, int streamNumber)
421{
422 if (streamNumber != 0 || trackCount(type) == 0)
423 return {};
424 QMediaMetaData md;
425 md.insert(QMediaMetaData::Language, QVariant::fromValue(QLocale::AnyLanguage));
426 return md;
427}
428
430{
431 QMediaMetaData md;
432 if (m_duration > 0)
433 md.insert(QMediaMetaData::Duration, m_duration);
434 if (m_videoWidth > 0 && m_videoHeight > 0)
435 md.insert(QMediaMetaData::Resolution, QSize{ m_videoWidth, m_videoHeight });
436 return md;
437}
438
439int QOhosMediaPlayer::activeTrack(TrackType type)
440{
441 return trackCount(type) > 0 ? 0 : -1;
442}
443
444void QOhosMediaPlayer::onVideoSurfaceReady()
445{
446 if (m_pendingSetMedia && !m_media.isEmpty()) {
447 m_pendingSetMedia = false;
448 m_sourceApplied = true;
449 applyPendingSource();
450 }
451}
452
453void QOhosMediaPlayer::handleStateChange(AVPlayerState newState)
454{
455 if (m_avState == newState)
456 return;
457 m_avState = newState;
458
459 // AV_IDLE is a transient state that OH_AVPlayer enters during Reset() as
460 // setMedia() is replacing the source. The user-facing NoMedia status is
461 // already emitted from setMedia(QUrl{}); leaking another NoMedia between a
462 // LoadingMedia/Invalid transition breaks observable status sequences.
463 if (newState == AV_IDLE)
464 return;
465
466 if (newState == AV_PREPARED) {
467 int32_t durationMs = 0;
468 if (OH_AVPlayer_GetDuration(m_player, &durationMs) == AV_ERR_OK) {
469 if (m_duration != durationMs) {
470 m_duration = durationMs;
471 durationChanged(m_duration);
472 }
473 }
474 m_seekable = m_duration > 0;
475 seekableChanged(m_seekable);
476
477 int32_t videoWidth = 0;
478 int32_t videoHeight = 0;
479 OH_AVPlayer_GetVideoWidth(m_player, &videoWidth);
480 OH_AVPlayer_GetVideoHeight(m_player, &videoHeight);
481 const bool hasVideo = videoWidth > 0 && videoHeight > 0;
482 if (hasVideo && m_videoOutput)
483 m_videoOutput->setVideoSize(QSize{ videoWidth, videoHeight });
484 videoAvailableChanged(hasVideo);
485 audioAvailableChanged(true);
486 const bool prevHasVideo = m_hasVideoTrack;
487 const bool prevHasAudio = m_hasAudioTrack;
488 m_hasVideoTrack = hasVideo;
489 m_hasAudioTrack = true;
490 if (prevHasVideo != m_hasVideoTrack || prevHasAudio != m_hasAudioTrack) {
491 tracksChanged();
492 activeTracksChanged();
493 }
494 m_videoWidth = videoWidth;
495 m_videoHeight = videoHeight;
496 metaDataChanged();
497 }
498
499 mediaStatusChanged(mediaStatusFor(newState, false));
500 stateChanged(playbackStateFor(newState));
501
502 if (newState == AV_PREPARED && m_pendingPlay) {
503 m_pendingPlay = false;
504 OH_AVPlayer_Play(m_player);
505 }
506}
507
508void QOhosMediaPlayer::handleEndOfStream()
509{
510 mediaStatusChanged(QMediaPlayer::EndOfMedia);
511 stateChanged(QMediaPlayer::StoppedState);
512}
513
514void QOhosMediaPlayer::handleSeekDone()
515{
516 // Position will refresh via AV_INFO_TYPE_POSITION_UPDATE.
517}
518
519void QOhosMediaPlayer::handleResolutionChange()
520{
521 if (!m_player)
522 return;
523 int32_t width = 0;
524 int32_t height = 0;
525 OH_AVPlayer_GetVideoWidth(m_player, &width);
526 OH_AVPlayer_GetVideoHeight(m_player, &height);
527 m_videoWidth = width;
528 m_videoHeight = height;
529 if (m_videoOutput && width > 0 && height > 0)
530 m_videoOutput->setVideoSize(QSize{ width, height });
531 videoAvailableChanged(width > 0 && height > 0);
532}
533
534void QOhosMediaPlayer::handleBufferingUpdate(int bufferingPercent)
535{
536 const float progress = std::clamp(bufferingPercent / 100.0f, 0.0f, 1.0f);
537 if (qFuzzyCompare(m_bufferProgress, progress))
538 return;
539 m_bufferProgress = progress;
540 bufferProgressChanged(progress);
541}
542
543void QOhosMediaPlayer::handleError(int32_t errorCode, const QString &errorMsg)
544{
545 qCWarning(qLcOhosMediaPlugin) << "OH_AVPlayer error" << errorCode << errorMsg;
546 setInvalidMediaWithError(QMediaPlayer::ResourceError,
547 errorMsg.isEmpty() ? QStringLiteral("AVPlayer error %1").arg(errorCode)
548 : errorMsg);
549}
550
551void QOhosMediaPlayer::releasePlayer()
552{
553 if (!m_player)
554 return;
555 OH_AVPlayer_SetOnInfoCallback(m_player, nullptr, nullptr);
556 OH_AVPlayer_SetOnErrorCallback(m_player, nullptr, nullptr);
557 OH_AVPlayer_ReleaseSync(m_player);
558 m_player = nullptr;
559 clearSource();
560}
561
562void QOhosMediaPlayer::clearSource()
563{
564 m_sourceFile.reset();
565 const bool changed = m_hasVideoTrack || m_hasAudioTrack;
566 m_hasVideoTrack = false;
567 m_hasAudioTrack = false;
568 const bool hadMetadata = m_duration > 0 || m_videoWidth > 0 || m_videoHeight > 0;
569 m_videoWidth = 0;
570 m_videoHeight = 0;
571 if (changed) {
572 tracksChanged();
573 activeTracksChanged();
574 }
575 if (hadMetadata)
576 metaDataChanged();
577}
578
579bool QOhosMediaPlayer::ensurePlayer()
580{
581 if (m_player)
582 return true;
583 m_player = OH_AVPlayer_Create();
584 if (!m_player) {
585 qCWarning(qLcOhosMediaPlugin) << "OH_AVPlayer_Create failed";
586 return false;
587 }
588 OH_AVPlayer_SetOnInfoCallback(m_player, &QOhosMediaPlayer::onInfoTrampoline, this);
589 OH_AVPlayer_SetOnErrorCallback(m_player, &QOhosMediaPlayer::onErrorTrampoline, this);
590 return true;
591}
592
593void QOhosMediaPlayer::applyVolume()
594{
595 if (!m_player)
596 return;
597
598 float volume = 1.0f;
599 if (m_audioOutput)
600 volume = m_audioOutput->muted ? 0.0f : m_audioOutput->volume;
601
602 OH_AVPlayer_SetVolume(m_player, volume, volume);
603}
604
605void QOhosMediaPlayer::onInfoTrampoline(OH_AVPlayer *player, AVPlayerOnInfoType type,
606 OH_AVFormat *body, void *userData)
607{
608 Q_UNUSED(body)
609 Q_UNUSED(player)
610 auto *self = reinterpret_cast<QOhosMediaPlayer *>(userData);
611 if (!self)
612 return;
613
614 switch (type) {
615 case AV_INFO_TYPE_STATE_CHANGE: {
616 AVPlayerState state{ AV_IDLE };
617 OH_AVPlayer_GetState(player, &state);
618 QMetaObject::invokeMethod(
619 self, [self, state]() { self->handleStateChange(state); }, Qt::QueuedConnection);
620 break;
621 }
622 case AV_INFO_TYPE_POSITION_UPDATE: {
623 int32_t pos = 0;
624 OH_AVPlayer_GetCurrentTime(player, &pos);
625 self->m_position.store(pos);
626 QMetaObject::invokeMethod(
627 self, [self, pos]() { self->positionChanged(qint64{ pos }); },
628 Qt::QueuedConnection);
629 break;
630 }
631 case AV_INFO_TYPE_DURATION_UPDATE: {
632 int32_t dur = 0;
633 OH_AVPlayer_GetDuration(player, &dur);
634 QMetaObject::invokeMethod(
635 self,
636 [self, dur]() {
637 if (self->m_duration == dur)
638 return;
639 self->m_duration = dur;
640 self->durationChanged(qint64{ dur });
641 },
642 Qt::QueuedConnection);
643 break;
644 }
645 case AV_INFO_TYPE_EOS:
646 QMetaObject::invokeMethod(
647 self, [self]() { self->handleEndOfStream(); }, Qt::QueuedConnection);
648 break;
649 case AV_INFO_TYPE_SEEKDONE:
650 QMetaObject::invokeMethod(
651 self, [self]() { self->handleSeekDone(); }, Qt::QueuedConnection);
652 break;
653 case AV_INFO_TYPE_RESOLUTION_CHANGE:
654 QMetaObject::invokeMethod(
655 self, [self]() { self->handleResolutionChange(); }, Qt::QueuedConnection);
656 break;
657 default:
658 break;
659 }
660}
661
662void QOhosMediaPlayer::onErrorTrampoline(OH_AVPlayer *, int32_t errorCode, const char *errorMsg,
663 void *userData)
664{
665 auto *self = reinterpret_cast<QOhosMediaPlayer *>(userData);
666 if (!self)
667 return;
668 QString message = errorMsg ? QString::fromUtf8(errorMsg) : QString{};
669 QMetaObject::invokeMethod(
670 self, [self, errorCode, message]() { self->handleError(errorCode, message); },
671 Qt::QueuedConnection);
672}
673
674QT_END_NAMESPACE
675
676#include "moc_qohosmediaplayer_p.cpp"
void stop() override
int activeTrack(TrackType type) override
qint64 position() const override
float bufferProgress() const override
void pause() override
void setAudioOutput(QPlatformAudioOutput *) override
void setPlaybackRate(qreal rate) override
void play() override
const QIODevice * mediaStream() const override
int trackCount(TrackType type) override
qreal playbackRate() const override
qint64 duration() const override
QUrl media() const override
void setVideoSink(QVideoSink *sink) override
void setPosition(qint64 position) override
QMediaMetaData trackMetaData(TrackType type, int streamNumber) override
QMediaTimeRange availablePlaybackRanges() const override
bool isSeekable() const override
void setMedia(const QUrl &media, QIODevice *stream) override
QMediaMetaData metaData() const override
Combined button and popup list for selecting options.
static QT_BEGIN_NAMESPACE constexpr int surfaceWaitTimeoutMs