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