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 "common/qohosvideosink_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 if (m_videoOutput && m_videoSink && !m_videoSink->rhi()) {
232 if (!m_surfaceWaitStarted) {
233 m_surfaceWaitStarted = true;
234 QTimer::singleShot(surfaceWaitTimeoutMs, this, [this] {
235 if (!m_sourceApplied && m_pendingSetMedia && !m_media.isEmpty()) {
236 m_pendingSetMedia = false;
237 m_sourceApplied = true;
238 applyPendingSource();
239 }
240 });
241 }
242 m_pendingSetMedia = true;
243 mediaStatusChanged(QMediaPlayer::LoadingMedia);
244 return;
245 }
246
247 m_sourceApplied = true;
248 applyPendingSource();
249}
250
251void QOhosMediaPlayer::applyPendingSource()
252{
253 if (!ensurePlayer()) {
254 setInvalidMediaWithError(QMediaPlayer::ResourceError,
255 QStringLiteral("Failed to create OH_AVPlayer"));
256 return;
257 }
258
259 OH_AVPlayer_SetAudioRendererInfo(m_player, AUDIOSTREAM_USAGE_MOVIE);
260
261 m_videoSurfaceAttached = false;
262
263 // Surface the LoadingMedia status before we hand the source to OH_AVPlayer
264 // so any synchronous SetURLSource/SetFDSource rejection still produces the
265 // observable Loading -> Invalid transition QMediaPlayer guarantees.
266 mediaStatusChanged(QMediaPlayer::LoadingMedia);
267
268 OH_AVErrCode result = AV_ERR_OK;
269 const QString scheme = m_media.scheme();
270 const bool isQrc = scheme == QLatin1String("qrc");
271 const bool isFileLike = m_media.isLocalFile() || scheme.isEmpty() || isQrc;
272 if (isFileLike) {
273 QString openPath;
274 if (m_media.isLocalFile()) {
275 openPath = m_media.toLocalFile();
276 } else if (isQrc) {
277 // QFile understands ":/path" for embedded resources.
278 openPath = QLatin1Char(':') + m_media.path();
279 } else {
280 // Bare relative paths.
281 openPath = m_media.toString();
282 }
283 auto file = std::make_unique<QFile>(openPath);
284 if (!file->open(QIODevice::ReadOnly)) {
285 setInvalidMediaWithError(QMediaPlayer::ResourceError,
286 QStringLiteral("Cannot open '%1'").arg(openPath));
287 return;
288 }
289 const int fd = file->handle();
290 const qint64 size = file->size();
291 if (fd < 0) {
292 // Resource-backed QFile is mapped, not file-descriptor-backed —
293 // AVPlayer needs a real fd, so spool to a temp file before handing
294 // it off.
295 auto temp = std::make_unique<QTemporaryFile>();
296 if (!temp->open() || temp->write(file->readAll()) < 0) {
297 setInvalidMediaWithError(QMediaPlayer::ResourceError,
298 QStringLiteral("Cannot stage '%1'").arg(openPath));
299 return;
300 }
301 temp->flush();
302 temp->seek(0);
303 const int tempFd = temp->handle();
304 const qint64 tempSize = temp->size();
305 result = OH_AVPlayer_SetFDSource(m_player, tempFd, 0, tempSize);
306 m_sourceFile = std::move(temp);
307 } else {
308 result = OH_AVPlayer_SetFDSource(m_player, fd, 0, size);
309 m_sourceFile = std::move(file);
310 }
311 } else {
312 const QByteArray url = m_media.toString(QUrl::FullyEncoded).toUtf8();
313 result = OH_AVPlayer_SetURLSource(m_player, url.constData());
314 }
315
316 if (result != AV_ERR_OK) {
317 qCWarning(qLcOhosMediaPlugin) << "Source rejected, error" << int(result);
318 setInvalidMediaWithError(QMediaPlayer::ResourceError,
319 QStringLiteral("OH_AVPlayer rejected the source"));
320 clearSource();
321 return;
322 }
323
324 if (m_videoOutput) {
325 if (auto *window = m_videoOutput->nativeWindow()) {
326 if (OH_AVPlayer_SetVideoSurface(m_player, window) == AV_ERR_OK)
327 m_videoSurfaceAttached = true;
328 else
329 qCWarning(qLcOhosMediaPlugin) << "OH_AVPlayer_SetVideoSurface failed";
330 }
331 }
332
333 result = OH_AVPlayer_Prepare(m_player);
334 if (result != AV_ERR_OK) {
335 qCWarning(qLcOhosMediaPlugin) << "Prepare failed, error" << int(result);
336 setInvalidMediaWithError(QMediaPlayer::ResourceError,
337 QStringLiteral("OH_AVPlayer_Prepare failed"));
338 clearSource();
339 }
340}
341
343{
344 if (!m_player) {
345 m_pendingPlay = !m_media.isEmpty();
346 return;
347 }
348
349 if (m_avState == AV_PREPARED || m_avState == AV_PAUSED || m_avState == AV_COMPLETED
350 || m_avState == AV_STOPPED) {
351 m_pendingPlay = false;
352 OH_AVPlayer_Play(m_player);
353 } else {
354 // Prepare hasn't finished yet — defer play to AV_INFO_TYPE_STATE_CHANGE
355 m_pendingPlay = true;
356 }
357}
358
360{
361 m_pendingPlay = false;
362 if (m_player && m_avState == AV_PLAYING)
363 OH_AVPlayer_Pause(m_player);
364}
365
367{
368 m_pendingPlay = false;
369 if (!m_player)
370 return;
371 if (m_avState == AV_PLAYING || m_avState == AV_PAUSED || m_avState == AV_PREPARED
372 || m_avState == AV_COMPLETED)
373 OH_AVPlayer_Stop(m_player);
374}
375
376void QOhosMediaPlayer::setAudioOutput(QPlatformAudioOutput *output)
377{
378 m_audioOutput = output;
379 applyVolume();
380}
381
382void QOhosMediaPlayer::setVideoSink(QVideoSink *sink)
383{
384 m_videoSink = sink;
385
386 if (!sink) {
387 m_videoOutput.reset();
388 return;
389 }
390
391 if (!m_videoOutput) {
392 m_videoOutput = std::make_unique<QOhosVideoOutput>(
393 sink, QOhosVideoOutput::ContentSource::MediaPlayer, this);
394 connect(m_videoOutput.get(), &QOhosVideoOutput::surfaceReady, this,
395 &QOhosMediaPlayer::onVideoSurfaceReady);
396 }
397}
398
399int QOhosMediaPlayer::trackCount(TrackType type)
400{
401 // OH_AVPlayer doesn't enumerate per-stream metadata up front; expose the
402 // detected presence of audio/video as a single default track so QMediaPlayer
403 // consumers can ask for activeTrack(0) and get a sensible answer.
404 switch (type) {
405 case VideoStream:
406 return m_hasVideoTrack ? 1 : 0;
407 case AudioStream:
408 return m_hasAudioTrack ? 1 : 0;
409 case SubtitleStream:
410 case NTrackTypes:
411 break;
412 }
413 return 0;
414}
415
416QMediaMetaData QOhosMediaPlayer::trackMetaData(TrackType type, int streamNumber)
417{
418 if (streamNumber != 0 || trackCount(type) == 0)
419 return {};
420 QMediaMetaData md;
421 md.insert(QMediaMetaData::Language, QVariant::fromValue(QLocale::AnyLanguage));
422 return md;
423}
424
426{
427 QMediaMetaData md;
428 if (m_duration > 0)
429 md.insert(QMediaMetaData::Duration, m_duration);
430 if (m_videoWidth > 0 && m_videoHeight > 0)
431 md.insert(QMediaMetaData::Resolution, QSize{ m_videoWidth, m_videoHeight });
432 return md;
433}
434
435int QOhosMediaPlayer::activeTrack(TrackType type)
436{
437 return trackCount(type) > 0 ? 0 : -1;
438}
439
440void QOhosMediaPlayer::onVideoSurfaceReady()
441{
442 if (m_pendingSetMedia && !m_media.isEmpty()) {
443 m_pendingSetMedia = false;
444 m_sourceApplied = true;
445 applyPendingSource();
446 }
447}
448
449void QOhosMediaPlayer::handleStateChange(AVPlayerState newState)
450{
451 if (m_avState == newState)
452 return;
453 m_avState = newState;
454
455 // AV_IDLE is a transient state that OH_AVPlayer enters during Reset() as
456 // setMedia() is replacing the source. The user-facing NoMedia status is
457 // already emitted from setMedia(QUrl{}); leaking another NoMedia between a
458 // LoadingMedia/Invalid transition breaks observable status sequences.
459 if (newState == AV_IDLE)
460 return;
461
462 if (newState == AV_PREPARED) {
463 int32_t durationMs = 0;
464 if (OH_AVPlayer_GetDuration(m_player, &durationMs) == AV_ERR_OK) {
465 if (m_duration != durationMs) {
466 m_duration = durationMs;
467 durationChanged(m_duration);
468 }
469 }
470 m_seekable = m_duration > 0;
471 seekableChanged(m_seekable);
472
473 int32_t videoWidth = 0;
474 int32_t videoHeight = 0;
475 OH_AVPlayer_GetVideoWidth(m_player, &videoWidth);
476 OH_AVPlayer_GetVideoHeight(m_player, &videoHeight);
477 const bool hasVideo = videoWidth > 0 && videoHeight > 0;
478 if (hasVideo && m_videoOutput)
479 m_videoOutput->setVideoSize(QSize{ videoWidth, videoHeight });
480 videoAvailableChanged(hasVideo);
481 audioAvailableChanged(true);
482 const bool prevHasVideo = m_hasVideoTrack;
483 const bool prevHasAudio = m_hasAudioTrack;
484 m_hasVideoTrack = hasVideo;
485 m_hasAudioTrack = true;
486 if (prevHasVideo != m_hasVideoTrack || prevHasAudio != m_hasAudioTrack) {
487 tracksChanged();
488 activeTracksChanged();
489 }
490 m_videoWidth = videoWidth;
491 m_videoHeight = videoHeight;
492 metaDataChanged();
493 }
494
495 mediaStatusChanged(mediaStatusFor(newState, false));
496 stateChanged(playbackStateFor(newState));
497
498 if (newState == AV_PREPARED && m_pendingPlay) {
499 m_pendingPlay = false;
500 OH_AVPlayer_Play(m_player);
501 }
502}
503
504void QOhosMediaPlayer::handleEndOfStream()
505{
506 mediaStatusChanged(QMediaPlayer::EndOfMedia);
507 stateChanged(QMediaPlayer::StoppedState);
508}
509
510void QOhosMediaPlayer::handleSeekDone()
511{
512 // Position will refresh via AV_INFO_TYPE_POSITION_UPDATE.
513}
514
515void QOhosMediaPlayer::handleResolutionChange()
516{
517 if (!m_player)
518 return;
519 int32_t width = 0;
520 int32_t height = 0;
521 OH_AVPlayer_GetVideoWidth(m_player, &width);
522 OH_AVPlayer_GetVideoHeight(m_player, &height);
523 m_videoWidth = width;
524 m_videoHeight = height;
525 if (m_videoOutput && width > 0 && height > 0)
526 m_videoOutput->setVideoSize(QSize{ width, height });
527 videoAvailableChanged(width > 0 && height > 0);
528}
529
530void QOhosMediaPlayer::handleBufferingUpdate(int bufferingPercent)
531{
532 const float progress = std::clamp(bufferingPercent / 100.0f, 0.0f, 1.0f);
533 if (qFuzzyCompare(m_bufferProgress, progress))
534 return;
535 m_bufferProgress = progress;
536 bufferProgressChanged(progress);
537}
538
539void QOhosMediaPlayer::handleError(int32_t errorCode, const QString &errorMsg)
540{
541 qCWarning(qLcOhosMediaPlugin) << "OH_AVPlayer error" << errorCode << errorMsg;
542 setInvalidMediaWithError(QMediaPlayer::ResourceError,
543 errorMsg.isEmpty() ? QStringLiteral("AVPlayer error %1").arg(errorCode)
544 : errorMsg);
545}
546
547void QOhosMediaPlayer::releasePlayer()
548{
549 if (!m_player)
550 return;
551 OH_AVPlayer_SetOnInfoCallback(m_player, nullptr, nullptr);
552 OH_AVPlayer_SetOnErrorCallback(m_player, nullptr, nullptr);
553 OH_AVPlayer_ReleaseSync(m_player);
554 m_player = nullptr;
555 clearSource();
556}
557
558void QOhosMediaPlayer::clearSource()
559{
560 m_sourceFile.reset();
561 const bool changed = m_hasVideoTrack || m_hasAudioTrack;
562 m_hasVideoTrack = false;
563 m_hasAudioTrack = false;
564 const bool hadMetadata = m_duration > 0 || m_videoWidth > 0 || m_videoHeight > 0;
565 m_videoWidth = 0;
566 m_videoHeight = 0;
567 if (changed) {
568 tracksChanged();
569 activeTracksChanged();
570 }
571 if (hadMetadata)
572 metaDataChanged();
573}
574
575bool QOhosMediaPlayer::ensurePlayer()
576{
577 if (m_player)
578 return true;
579 m_player = OH_AVPlayer_Create();
580 if (!m_player) {
581 qCWarning(qLcOhosMediaPlugin) << "OH_AVPlayer_Create failed";
582 return false;
583 }
584 OH_AVPlayer_SetOnInfoCallback(m_player, &QOhosMediaPlayer::onInfoTrampoline, this);
585 OH_AVPlayer_SetOnErrorCallback(m_player, &QOhosMediaPlayer::onErrorTrampoline, this);
586 return true;
587}
588
589void QOhosMediaPlayer::applyVolume()
590{
591 if (!m_player)
592 return;
593
594 float volume = 1.0f;
595 if (m_audioOutput)
596 volume = m_audioOutput->muted ? 0.0f : m_audioOutput->volume;
597
598 OH_AVPlayer_SetVolume(m_player, volume, volume);
599}
600
601void QOhosMediaPlayer::onInfoTrampoline(OH_AVPlayer *player, AVPlayerOnInfoType type,
602 OH_AVFormat *body, void *userData)
603{
604 Q_UNUSED(body)
605 Q_UNUSED(player)
606 auto *self = reinterpret_cast<QOhosMediaPlayer *>(userData);
607 if (!self)
608 return;
609
610 switch (type) {
611 case AV_INFO_TYPE_STATE_CHANGE: {
612 AVPlayerState state{ AV_IDLE };
613 OH_AVPlayer_GetState(player, &state);
614 QMetaObject::invokeMethod(
615 self, [self, state]() { self->handleStateChange(state); }, Qt::QueuedConnection);
616 break;
617 }
618 case AV_INFO_TYPE_POSITION_UPDATE: {
619 int32_t pos = 0;
620 OH_AVPlayer_GetCurrentTime(player, &pos);
621 self->m_position.store(pos);
622 QMetaObject::invokeMethod(
623 self, [self, pos]() { self->positionChanged(qint64{ pos }); },
624 Qt::QueuedConnection);
625 break;
626 }
627 case AV_INFO_TYPE_DURATION_UPDATE: {
628 int32_t dur = 0;
629 OH_AVPlayer_GetDuration(player, &dur);
630 QMetaObject::invokeMethod(
631 self,
632 [self, dur]() {
633 if (self->m_duration == dur)
634 return;
635 self->m_duration = dur;
636 self->durationChanged(qint64{ dur });
637 },
638 Qt::QueuedConnection);
639 break;
640 }
641 case AV_INFO_TYPE_EOS:
642 QMetaObject::invokeMethod(
643 self, [self]() { self->handleEndOfStream(); }, Qt::QueuedConnection);
644 break;
645 case AV_INFO_TYPE_SEEKDONE:
646 QMetaObject::invokeMethod(
647 self, [self]() { self->handleSeekDone(); }, Qt::QueuedConnection);
648 break;
649 case AV_INFO_TYPE_RESOLUTION_CHANGE:
650 QMetaObject::invokeMethod(
651 self, [self]() { self->handleResolutionChange(); }, Qt::QueuedConnection);
652 break;
653 default:
654 break;
655 }
656}
657
658void QOhosMediaPlayer::onErrorTrampoline(OH_AVPlayer *, int32_t errorCode, const char *errorMsg,
659 void *userData)
660{
661 auto *self = reinterpret_cast<QOhosMediaPlayer *>(userData);
662 if (!self)
663 return;
664 QString message = errorMsg ? QString::fromUtf8(errorMsg) : QString{};
665 QMetaObject::invokeMethod(
666 self, [self, errorCode, message]() { self->handleError(errorCode, message); },
667 Qt::QueuedConnection);
668}
669
670QT_END_NAMESPACE
671
672#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