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 TrackType::VideoStream:
406 return m_hasVideoTrack ? 1 : 0;
407 case TrackType::AudioStream:
408 return m_hasAudioTrack ? 1 : 0;
409 case TrackType::SubtitleStream:
410 break;
411 }
412 return 0;
413}
414
415QMediaMetaData QOhosMediaPlayer::trackMetaData(TrackType type, int streamNumber)
416{
417 if (streamNumber != 0 || trackCount(type) == 0)
418 return {};
419 QMediaMetaData md;
420 md.insert(QMediaMetaData::Language, QVariant::fromValue(QLocale::AnyLanguage));
421 return md;
422}
423
425{
426 QMediaMetaData md;
427 if (m_duration > 0)
428 md.insert(QMediaMetaData::Duration, m_duration);
429 if (m_videoWidth > 0 && m_videoHeight > 0)
430 md.insert(QMediaMetaData::Resolution, QSize{ m_videoWidth, m_videoHeight });
431 return md;
432}
433
434int QOhosMediaPlayer::activeTrack(TrackType type)
435{
436 return trackCount(type) > 0 ? 0 : -1;
437}
438
439void QOhosMediaPlayer::onVideoSurfaceReady()
440{
441 if (m_pendingSetMedia && !m_media.isEmpty()) {
442 m_pendingSetMedia = false;
443 m_sourceApplied = true;
444 applyPendingSource();
445 }
446}
447
448void QOhosMediaPlayer::handleStateChange(AVPlayerState newState)
449{
450 if (m_avState == newState)
451 return;
452 m_avState = newState;
453
454 // AV_IDLE is a transient state that OH_AVPlayer enters during Reset() as
455 // setMedia() is replacing the source. The user-facing NoMedia status is
456 // already emitted from setMedia(QUrl{}); leaking another NoMedia between a
457 // LoadingMedia/Invalid transition breaks observable status sequences.
458 if (newState == AV_IDLE)
459 return;
460
461 if (newState == AV_PREPARED) {
462 int32_t durationMs = 0;
463 if (OH_AVPlayer_GetDuration(m_player, &durationMs) == AV_ERR_OK) {
464 if (m_duration != durationMs) {
465 m_duration = durationMs;
466 durationChanged(m_duration);
467 }
468 }
469 m_seekable = m_duration > 0;
470 seekableChanged(m_seekable);
471
472 int32_t videoWidth = 0;
473 int32_t videoHeight = 0;
474 OH_AVPlayer_GetVideoWidth(m_player, &videoWidth);
475 OH_AVPlayer_GetVideoHeight(m_player, &videoHeight);
476 const bool hasVideo = videoWidth > 0 && videoHeight > 0;
477 if (hasVideo && m_videoOutput)
478 m_videoOutput->setVideoSize(QSize{ videoWidth, videoHeight });
479 videoAvailableChanged(hasVideo);
480 audioAvailableChanged(true);
481 const bool prevHasVideo = m_hasVideoTrack;
482 const bool prevHasAudio = m_hasAudioTrack;
483 m_hasVideoTrack = hasVideo;
484 m_hasAudioTrack = true;
485 if (prevHasVideo != m_hasVideoTrack || prevHasAudio != m_hasAudioTrack) {
486 tracksChanged();
487 activeTracksChanged();
488 }
489 m_videoWidth = videoWidth;
490 m_videoHeight = videoHeight;
491 metaDataChanged();
492 }
493
494 mediaStatusChanged(mediaStatusFor(newState, false));
495 stateChanged(playbackStateFor(newState));
496
497 if (newState == AV_PREPARED && m_pendingPlay) {
498 m_pendingPlay = false;
499 OH_AVPlayer_Play(m_player);
500 }
501}
502
503void QOhosMediaPlayer::handleEndOfStream()
504{
505 mediaStatusChanged(QMediaPlayer::EndOfMedia);
506 stateChanged(QMediaPlayer::StoppedState);
507}
508
509void QOhosMediaPlayer::handleSeekDone()
510{
511 // Position will refresh via AV_INFO_TYPE_POSITION_UPDATE.
512}
513
514void QOhosMediaPlayer::handleResolutionChange()
515{
516 if (!m_player)
517 return;
518 int32_t width = 0;
519 int32_t height = 0;
520 OH_AVPlayer_GetVideoWidth(m_player, &width);
521 OH_AVPlayer_GetVideoHeight(m_player, &height);
522 m_videoWidth = width;
523 m_videoHeight = height;
524 if (m_videoOutput && width > 0 && height > 0)
525 m_videoOutput->setVideoSize(QSize{ width, height });
526 videoAvailableChanged(width > 0 && height > 0);
527}
528
529void QOhosMediaPlayer::handleBufferingUpdate(int bufferingPercent)
530{
531 const float progress = std::clamp(bufferingPercent / 100.0f, 0.0f, 1.0f);
532 if (qFuzzyCompare(m_bufferProgress, progress))
533 return;
534 m_bufferProgress = progress;
535 bufferProgressChanged(progress);
536}
537
538void QOhosMediaPlayer::handleError(int32_t errorCode, const QString &errorMsg)
539{
540 qCWarning(qLcOhosMediaPlugin) << "OH_AVPlayer error" << errorCode << errorMsg;
541 setInvalidMediaWithError(QMediaPlayer::ResourceError,
542 errorMsg.isEmpty() ? QStringLiteral("AVPlayer error %1").arg(errorCode)
543 : errorMsg);
544}
545
546void QOhosMediaPlayer::releasePlayer()
547{
548 if (!m_player)
549 return;
550 OH_AVPlayer_SetOnInfoCallback(m_player, nullptr, nullptr);
551 OH_AVPlayer_SetOnErrorCallback(m_player, nullptr, nullptr);
552 OH_AVPlayer_ReleaseSync(m_player);
553 m_player = nullptr;
554 clearSource();
555}
556
557void QOhosMediaPlayer::clearSource()
558{
559 m_sourceFile.reset();
560 const bool changed = m_hasVideoTrack || m_hasAudioTrack;
561 m_hasVideoTrack = false;
562 m_hasAudioTrack = false;
563 const bool hadMetadata = m_duration > 0 || m_videoWidth > 0 || m_videoHeight > 0;
564 m_videoWidth = 0;
565 m_videoHeight = 0;
566 if (changed) {
567 tracksChanged();
568 activeTracksChanged();
569 }
570 if (hadMetadata)
571 metaDataChanged();
572}
573
574bool QOhosMediaPlayer::ensurePlayer()
575{
576 if (m_player)
577 return true;
578 m_player = OH_AVPlayer_Create();
579 if (!m_player) {
580 qCWarning(qLcOhosMediaPlugin) << "OH_AVPlayer_Create failed";
581 return false;
582 }
583 OH_AVPlayer_SetOnInfoCallback(m_player, &QOhosMediaPlayer::onInfoTrampoline, this);
584 OH_AVPlayer_SetOnErrorCallback(m_player, &QOhosMediaPlayer::onErrorTrampoline, this);
585 return true;
586}
587
588void QOhosMediaPlayer::applyVolume()
589{
590 if (!m_player)
591 return;
592
593 float volume = 1.0f;
594 if (m_audioOutput)
595 volume = m_audioOutput->muted ? 0.0f : m_audioOutput->volume;
596
597 OH_AVPlayer_SetVolume(m_player, volume, volume);
598}
599
600void QOhosMediaPlayer::onInfoTrampoline(OH_AVPlayer *player, AVPlayerOnInfoType type,
601 OH_AVFormat *body, void *userData)
602{
603 Q_UNUSED(body)
604 Q_UNUSED(player)
605 auto *self = reinterpret_cast<QOhosMediaPlayer *>(userData);
606 if (!self)
607 return;
608
609 switch (type) {
610 case AV_INFO_TYPE_STATE_CHANGE: {
611 AVPlayerState state{ AV_IDLE };
612 OH_AVPlayer_GetState(player, &state);
613 QMetaObject::invokeMethod(
614 self, [self, state]() { self->handleStateChange(state); }, Qt::QueuedConnection);
615 break;
616 }
617 case AV_INFO_TYPE_POSITION_UPDATE: {
618 int32_t pos = 0;
619 OH_AVPlayer_GetCurrentTime(player, &pos);
620 self->m_position.store(pos);
621 QMetaObject::invokeMethod(
622 self, [self, pos]() { self->positionChanged(qint64{ pos }); },
623 Qt::QueuedConnection);
624 break;
625 }
626 case AV_INFO_TYPE_DURATION_UPDATE: {
627 int32_t dur = 0;
628 OH_AVPlayer_GetDuration(player, &dur);
629 QMetaObject::invokeMethod(
630 self,
631 [self, dur]() {
632 if (self->m_duration == dur)
633 return;
634 self->m_duration = dur;
635 self->durationChanged(qint64{ dur });
636 },
637 Qt::QueuedConnection);
638 break;
639 }
640 case AV_INFO_TYPE_EOS:
641 QMetaObject::invokeMethod(
642 self, [self]() { self->handleEndOfStream(); }, Qt::QueuedConnection);
643 break;
644 case AV_INFO_TYPE_SEEKDONE:
645 QMetaObject::invokeMethod(
646 self, [self]() { self->handleSeekDone(); }, Qt::QueuedConnection);
647 break;
648 case AV_INFO_TYPE_RESOLUTION_CHANGE:
649 QMetaObject::invokeMethod(
650 self, [self]() { self->handleResolutionChange(); }, Qt::QueuedConnection);
651 break;
652 default:
653 break;
654 }
655}
656
657void QOhosMediaPlayer::onErrorTrampoline(OH_AVPlayer *, int32_t errorCode, const char *errorMsg,
658 void *userData)
659{
660 auto *self = reinterpret_cast<QOhosMediaPlayer *>(userData);
661 if (!self)
662 return;
663 QString message = errorMsg ? QString::fromUtf8(errorMsg) : QString{};
664 QMetaObject::invokeMethod(
665 self, [self, errorCode, message]() { self->handleError(errorCode, message); },
666 Qt::QueuedConnection);
667}
668
669QT_END_NAMESPACE
670
671#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