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
qmediaengineplayer.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
6#include <common/mfmetadata_p.h>
7#include <mfstream_p.h>
8#include <player/qmediaenginenotify_p.h>
9#include <player/qmediaenginetimedtextnotify_p.h>
10#include <player/qmediaenginevideobuffer_p.h>
11
12#include <QtMultimedia/qaudiooutput.h>
13#include <QtMultimedia/qvideosink.h>
14#include <QtMultimedia/private/qcomtaskresource_p.h>
15#include <QtMultimedia/private/qplatformaudiooutput_p.h>
16#include <QtMultimedia/private/qplatformvideosink_p.h>
17#include <QtMultimedia/private/qvideoframe_p.h>
18#include <QtMultimedia/private/qwindows_scopedpropvariant_p.h>
19#include <QtMultimedia/private/qwindowsd3d11rhitextureimporter_p.h>
20#include <QtMultimedia/private/qwmf_support_p.h>
21#include <QtConcurrent/qtconcurrentrun.h>
22#include <QtCore/qfile.h>
23#include <QtCore/qlocale.h>
24#include <QtCore/qloggingcategory.h>
25#include <QtCore/qscopeguard.h>
26#include <QtCore/qtimer.h>
27#include <QtCore/private/qbstr_p.h>
28#include <QtCore/private/qsystemerror_p.h>
29
30#include <dxgi.h>
31#include <mfapi.h>
32#include <mfidl.h>
33
34Q_LOGGING_CATEGORY(lcMediaEngine, "qt.multimedia.mediaengine")
35
36QT_BEGIN_NAMESPACE
37
38using namespace Qt::StringLiterals;
39using namespace std::chrono_literals;
40using QtMultimediaPrivate::ScopedPropVariant;
41using reference_time = QWMF::reference_time;
42
43namespace {
44
45QVariant languageVariant(const QString &langStr)
46{
47 const QLocale::Language lang = QLocale::codeToLanguage(langStr, QLocale::AnyLanguageCode);
48 return lang != QLocale::AnyLanguage ? QVariant::fromValue(lang) : QVariant(langStr);
49}
50
51QMediaPlayer::Error mapMediaEngineError(MF_MEDIA_ENGINE_ERR err)
52{
53 switch (err) {
54 case MF_MEDIA_ENGINE_ERR_NOERROR:
55 return QMediaPlayer::NoError;
56 case MF_MEDIA_ENGINE_ERR_ABORTED:
57 return QMediaPlayer::ResourceError;
58 case MF_MEDIA_ENGINE_ERR_NETWORK:
59 return QMediaPlayer::NetworkError;
60 case MF_MEDIA_ENGINE_ERR_DECODE:
61 return QMediaPlayer::FormatError;
62 case MF_MEDIA_ENGINE_ERR_SRC_NOT_SUPPORTED:
63 return QMediaPlayer::ResourceError;
64 case MF_MEDIA_ENGINE_ERR_ENCRYPTED:
65 return QMediaPlayer::AccessDeniedError;
66 default:
67 return QMediaPlayer::ResourceError;
68 }
69}
70
71std::chrono::microseconds videoFrameDurationUs(IMFMediaEngineEx *engine)
72{
73 if (!engine)
74 return {};
75
76 DWORD streamCount = 0;
77 if (FAILED(engine->GetNumberOfStreams(&streamCount)))
78 return {};
79
80 for (DWORD i = 0; i < streamCount; ++i) {
81 ScopedPropVariant majorType;
82 if (FAILED(engine->GetStreamAttribute(i, MF_MT_MAJOR_TYPE, majorType.get()))
83 || majorType->vt != VT_CLSID || *majorType->puuid != MFMediaType_Video) {
84 continue;
85 }
86
87 ScopedPropVariant rate;
88 if (FAILED(engine->GetStreamAttribute(i, MF_MT_FRAME_RATE, rate.get()))
89 || rate->vt != VT_UI8) {
90 return {};
91 }
92
93 UINT64 avgTimePerFrame = 0;
94 if (SUCCEEDED(MFFrameRateToAverageTimePerFrame(
95 HI32(rate->uhVal.QuadPart), LO32(rate->uhVal.QuadPart), &avgTimePerFrame)))
96 return std::chrono::round<std::chrono::microseconds>(reference_time(avgTimePerFrame));
97
98 return {};
99 }
100 return {};
101}
102
103} // namespace
104
105bool QMediaEnginePlayer::initD3D11()
106{
107 m_textureImporter = std::make_shared<QtMultimediaPrivate::QWindowsD3D11RhiTextureImporter>();
108
109 const D3D_FEATURE_LEVEL featureLevels[] = {
110 D3D_FEATURE_LEVEL_11_1,
111 D3D_FEATURE_LEVEL_11_0,
112 };
113
114 UINT flags = D3D11_CREATE_DEVICE_VIDEO_SUPPORT | D3D11_CREATE_DEVICE_BGRA_SUPPORT;
115
116 HRESULT hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, flags, featureLevels,
117 ARRAYSIZE(featureLevels), D3D11_SDK_VERSION, &m_d3dDevice,
118 nullptr, nullptr);
119
120 if (FAILED(hr)) {
121 qCWarning(lcMediaEngine) << "D3D11CreateDevice:" << QSystemError::windowsComString(hr);
122 return false;
123 }
124
125 ComPtr<ID3D10Multithread> multithread;
126 if (SUCCEEDED(m_d3dDevice.As(&multithread)))
127 multithread->SetMultithreadProtected(TRUE);
128
129 return true;
130}
131
132bool QMediaEnginePlayer::initMediaEngine()
133{
134 HRESULT hr = MFCreateDXGIDeviceManager(&m_resetToken, &m_dxgiManager);
135 if (FAILED(hr)) {
136 qCWarning(lcMediaEngine) << "MFCreateDXGIDeviceManager:"
137 << QSystemError::windowsComString(hr);
138 return false;
139 }
140
141 hr = m_dxgiManager->ResetDevice(m_d3dDevice.Get(), m_resetToken);
142 if (FAILED(hr)) {
143 qCWarning(lcMediaEngine) << "ResetDevice:" << QSystemError::windowsComString(hr);
144 return false;
145 }
146
147 m_notify = makeComObject<QMediaEngineNotify>();
148 // Queued: some MF events fire synchronously (e.g. PAUSE from Pause()),
149 // must defer to avoid re-entering handleMediaEngineEvent mid-operation.
150 QObject::connect(m_notify.Get(), &QMediaEngineNotify::eventNotified, context(),
151 [this](DWORD event, DWORD_PTR param1, DWORD param2) {
152 handleMediaEngineEvent(event, param1, param2);
153 }, Qt::QueuedConnection);
154
155 ComPtr<IMFAttributes> attrs;
156 hr = MFCreateAttributes(&attrs, 4);
157 if (FAILED(hr)) {
158 qCWarning(lcMediaEngine) << "MFCreateAttributes:" << QSystemError::windowsComString(hr);
159 return false;
160 }
161
162 attrs->SetUnknown(MF_MEDIA_ENGINE_CALLBACK, m_notify.Get());
163 attrs->SetUnknown(MF_MEDIA_ENGINE_DXGI_MANAGER, m_dxgiManager.Get());
164 attrs->SetUINT32(MF_MEDIA_ENGINE_VIDEO_OUTPUT_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM);
165
166 ComPtr<IMFMediaEngineClassFactory> factory;
167 hr = CoCreateInstance(CLSID_MFMediaEngineClassFactory, nullptr, CLSCTX_INPROC_SERVER,
168 IID_PPV_ARGS(&factory));
169 if (FAILED(hr)) {
170 qCWarning(lcMediaEngine) << "CoCreateInstance(MFMediaEngineClassFactory):"
171 << QSystemError::windowsComString(hr);
172 return false;
173 }
174
175 hr = factory->CreateInstance(0, attrs.Get(), &m_mediaEngine);
176 if (FAILED(hr)) {
177 qCWarning(lcMediaEngine) << "IMFMediaEngine::CreateInstance:"
178 << QSystemError::windowsComString(hr);
179 return false;
180 }
181
182 hr = m_mediaEngine.As(&m_mediaEngineEx);
183 if (FAILED(hr)) {
184 qCWarning(lcMediaEngine) << "IMFMediaEngineEx:" << QSystemError::windowsComString(hr);
185 return false;
186 }
187
188 // Enable inband timed text (embedded subtitles) discovery.
189 // IMFTimedText is obtained via MFGetService with MF_MEDIA_ENGINE_TIMEDTEXT.
190 ComPtr<IMFGetService> getService;
191 if (SUCCEEDED(m_mediaEngine.As(&getService))) {
192 getService->GetService(MF_MEDIA_ENGINE_TIMEDTEXT, IID_PPV_ARGS(&m_timedText));
193 }
194
195 if (m_timedText) {
196 m_timedText->SetInBandEnabled(TRUE);
197
198 m_timedTextNotify = makeComObject<QMediaEngineTimedTextNotify>();
199
200 QObject::connect(m_timedTextNotify.Get(), &QMediaEngineTimedTextNotify::trackAdded,
201 context(), [this] {
202 updateSubtitleTracks();
203 });
204
205 QObject::connect(m_timedTextNotify.Get(), &QMediaEngineTimedTextNotify::subtitleTextChanged,
206 context(), [this](const QString &text) {
207 if (m_videoSink)
208 m_videoSink->setSubtitleText(text);
209 });
210
211 m_timedText->RegisterNotifications(m_timedTextNotify.Get());
212 }
213
214 // Apply cached playback rate to the newly created engine.
215 if (!qFuzzyCompare(m_playbackRate, 1.0)) {
216 m_mediaEngine->SetDefaultPlaybackRate(m_playbackRate);
217 m_mediaEngine->SetPlaybackRate(m_playbackRate);
218 }
219
220 return true;
221}
222
223void QMediaEnginePlayer::releaseMediaEngine()
224{
225 if (m_mediaEngine) {
226 m_mediaEngine->Shutdown();
227 m_mediaEngine.Reset();
228 }
229 m_mediaEngineEx.Reset();
230 m_timedText.Reset();
231 m_timedTextNotify.Reset();
232 m_notify.Reset();
233 m_dxgiManager.Reset();
234 m_resetToken = 0;
235}
236
237void QMediaEnginePlayer::startPlaybackTimers()
238{
239 m_positionUpdateTimer.start();
240 m_videoRenderTimer.start();
241}
242
243void QMediaEnginePlayer::stopPlaybackTimers()
244{
245 m_positionUpdateTimer.stop();
246 m_videoRenderTimer.stop();
247}
248
250{
251 if (!initD3D11())
252 error(QMediaPlayer::ResourceError, u"Failed to initialize D3D11"_s);
253
254 m_positionUpdateTimer.callOnTimeout(context(), [this] {
255 if (m_mediaEngine && !m_seekTarget)
256 positionChanged(currentPositionMs());
257 });
258
259 m_videoRenderTimer.callOnTimeout(context(), [this] {
260 presentVideoFrame();
261 });
262
263 m_seekTimeoutTimer.setSingleShot(true);
264 m_seekTimeoutTimer.callOnTimeout(context(), [this] {
265 if (m_seekTarget) {
266 qCWarning(lcMediaEngine) << "Seek timeout — SEEKED event not received, resetting";
267 m_seekTarget.reset();
268 }
269 });
270}
271
273{
274 stopPlaybackTimers();
275 m_seekTimeoutTimer.stop();
276
277 m_metadataFuture.cancel();
278
279 releaseMediaEngine();
280}
281
282void QMediaEnginePlayer::handleMediaEngineEvent(DWORD event, DWORD_PTR param1, DWORD param2)
283{
284 Q_UNUSED(param2);
285
286 if (!m_mediaEngine)
287 return;
288
289 switch (event) {
290 case MF_MEDIA_ENGINE_EVENT_LOADEDMETADATA:
291 qCDebug(lcMediaEngine) << "LOADEDMETADATA";
292 updateMetaData();
293 updateTracks();
294 updateDuration();
295 break;
296
297 case MF_MEDIA_ENGINE_EVENT_LOADEDDATA: {
298 qCDebug(lcMediaEngine) << "LOADEDDATA";
299 audioAvailableChanged(m_mediaEngine->HasAudio());
300 videoAvailableChanged(m_mediaEngine->HasVideo());
301 updateVideoFrameFormat();
302 updateNativeSize();
303 seekableChanged(!m_stream || !m_stream->isSequential());
304 mediaStatusChanged(QMediaPlayer::LoadedMedia);
305
306 if (const float bp = bufferProgress(); bp > 0)
307 bufferProgressChanged(bp);
308
309 if (m_pendingPlay) {
310 m_pendingPlay = false;
311 play();
312 }
313 break;
314 }
315
316 case MF_MEDIA_ENGINE_EVENT_CANPLAY:
317 qCDebug(lcMediaEngine) << "CANPLAY";
318 if (state() == QMediaPlayer::PlayingState || mediaStatus() == QMediaPlayer::BufferingMedia
319 || m_pendingPlay) {
320 mediaStatusChanged(QMediaPlayer::BufferedMedia);
321 }
322 bufferProgressChanged(1.0f);
323 break;
324
325 case MF_MEDIA_ENGINE_EVENT_PLAYING:
326 qCDebug(lcMediaEngine) << "PLAYING";
327 if (state() != QMediaPlayer::PlayingState)
328 stateChanged(QMediaPlayer::PlayingState);
329 if (mediaStatus() == QMediaPlayer::BufferingMedia)
330 mediaStatusChanged(QMediaPlayer::BufferedMedia);
331 startPlaybackTimers();
332 break;
333
334 case MF_MEDIA_ENGINE_EVENT_PAUSE:
335 qCDebug(lcMediaEngine) << "PAUSE";
336 // Events are queued: ignore a stale PAUSE if playback was resumed meanwhile.
337 if (!m_mediaEngine->IsPaused())
338 break;
339 if (state() != QMediaPlayer::StoppedState && state() != QMediaPlayer::PausedState) {
340 stateChanged(QMediaPlayer::PausedState);
341 stopPlaybackTimers();
342 }
343 break;
344
345 case MF_MEDIA_ENGINE_EVENT_ENDED: {
346 qCDebug(lcMediaEngine) << "ENDED";
347 stopPlaybackTimers();
348
349 if (mediaStatus() == QMediaPlayer::BufferingMedia)
350 mediaStatusChanged(QMediaPlayer::BufferedMedia);
351
352 if (doLoop()) {
353 m_mediaEngine->SetCurrentTime(0);
354 m_mediaEngine->Play();
355 } else {
356 if (m_videoSink)
357 m_videoSink->setVideoFrame({});
358 positionChanged(m_duration.count());
359 stateChanged(QMediaPlayer::StoppedState);
360 mediaStatusChanged(QMediaPlayer::EndOfMedia);
361 bufferProgressChanged(0.f);
362 resetCurrentLoop();
363 }
364 break;
365 }
366
367 case MF_MEDIA_ENGINE_EVENT_TIMEUPDATE:
368 if (!m_seekTarget)
369 positionChanged(currentPositionMs());
370 break;
371
372 case MF_MEDIA_ENGINE_EVENT_ERROR: {
373 const auto mfErr = static_cast<MF_MEDIA_ENGINE_ERR>(param1);
374
375 QString detail;
376 switch (mfErr) {
377 case MF_MEDIA_ENGINE_ERR_ABORTED:
378 detail = u"Playback aborted"_s;
379 break;
380 case MF_MEDIA_ENGINE_ERR_NETWORK:
381 detail = u"Network error"_s;
382 break;
383 case MF_MEDIA_ENGINE_ERR_DECODE:
384 detail = u"Unsupported or corrupt media"_s;
385 break;
386 case MF_MEDIA_ENGINE_ERR_SRC_NOT_SUPPORTED:
387 detail = u"Source not supported"_s;
388 break;
389 case MF_MEDIA_ENGINE_ERR_ENCRYPTED:
390 detail = u"Encrypted content"_s;
391 break;
392 default:
393 detail = u"Unknown error %1"_s.arg(param1);
394 break;
395 }
396
397 qCWarning(lcMediaEngine) << "Error:" << detail;
398 error(mapMediaEngineError(mfErr), detail);
399 mediaStatusChanged(QMediaPlayer::InvalidMedia);
400 stopPlaybackTimers();
401 break;
402 }
403
404 case MF_MEDIA_ENGINE_EVENT_BUFFERINGSTARTED:
405 qCDebug(lcMediaEngine) << "BUFFERINGSTARTED";
406 mediaStatusChanged(QMediaPlayer::BufferingMedia);
407 bufferProgressChanged(bufferProgress());
408 break;
409
410 case MF_MEDIA_ENGINE_EVENT_BUFFERINGENDED:
411 qCDebug(lcMediaEngine) << "BUFFERINGENDED";
412 mediaStatusChanged(QMediaPlayer::BufferedMedia);
413 bufferProgressChanged(bufferProgress());
414 break;
415
416 case MF_MEDIA_ENGINE_EVENT_DURATIONCHANGE:
417 qCDebug(lcMediaEngine) << "DURATIONCHANGE";
418 updateDuration();
419 break;
420
421 case MF_MEDIA_ENGINE_EVENT_FORMATCHANGE:
422 qCDebug(lcMediaEngine) << "FORMATCHANGE";
423 updateVideoFrameFormat();
424 break;
425
426 case MF_MEDIA_ENGINE_EVENT_FIRSTFRAMEREADY:
427 qCDebug(lcMediaEngine) << "FIRSTFRAMEREADY";
428 if (state() != QMediaPlayer::StoppedState)
429 presentVideoFrame();
430 break;
431
432 case MF_MEDIA_ENGINE_EVENT_SEEKED:
433 qCDebug(lcMediaEngine) << "SEEKED";
434 m_seekTimeoutTimer.stop();
435 m_seekTarget.reset();
436 break;
437
438 default:
439 break;
440 }
441}
442
443void QMediaEnginePlayer::setMedia(const QUrl &media, QIODevice *stream)
444{
445 qCDebug(lcMediaEngine) << "setMedia:" << media << "stream:" << stream;
446
447 if (state() != QMediaPlayer::StoppedState)
448 stop();
449
450 m_url = media;
451 m_stream = stream;
452
453 m_duration = {};
454 m_pendingPlay = false;
455 m_seekTarget.reset();
456 m_seekTimeoutTimer.stop();
457 seekableChanged(false);
458 m_videoSize = {};
459 m_transferTexture.Reset();
460 m_metaData = {};
461 m_metadataFuture.cancel();
462
463 if (m_videoSink)
464 m_videoSink->setVideoFrame({});
465
466 m_tracks = {};
467
468 if (media.isEmpty() && !stream) {
469 audioAvailableChanged(false);
470 videoAvailableChanged(false);
471 // IMFMediaEngine has no API to clear the current source without
472 // shutting down. Release the engine now; it will be lazily
473 // recreated on the next setMedia() call with a real source.
474 releaseMediaEngine();
475 metaDataChanged();
476 mediaStatusChanged(QMediaPlayer::NoMedia);
477 return;
478 }
479
480 // Lazy init: create the MediaEngine on first use,
481 // or recreate it after a previous NoMedia release.
482 if (!m_mediaEngine) {
483 if (!initMediaEngine()) {
484 error(QMediaPlayer::ResourceError, u"MediaEngine not initialized"_s);
485 mediaStatusChanged(QMediaPlayer::InvalidMedia);
486 return;
487 }
488 }
489
490 HRESULT hr = E_FAIL;
491
492 if (stream) {
493 ComPtr<MFStream> mfStream = makeComObject<MFStream>(stream, false);
494 QString urlStr = media.toString();
495 // SetSourceFromByteStream requires a non-null URL.
496 // Use the real file name when available so that Media Foundation
497 // can infer the MIME type from the extension.
498 if (urlStr.isEmpty()) {
499 if (auto *file = qobject_cast<QFile *>(stream))
500 urlStr = u"file:///"_s + file->fileName();
501 else
502 urlStr = u"stream:///media"_s;
503 }
504 const QBStr bstrUrl(urlStr);
505 hr = m_mediaEngineEx->SetSourceFromByteStream(mfStream.Get(), bstrUrl.bstr());
506 } else {
507 const QBStr bstrUrl(media.toString());
508 hr = m_mediaEngine->SetSource(bstrUrl.bstr());
509 }
510
511 if (FAILED(hr)) {
512 qCWarning(lcMediaEngine) << "SetSource:" << QSystemError::windowsComString(hr);
513 error(QMediaPlayer::ResourceError, u"Failed to set media source"_s);
514 mediaStatusChanged(QMediaPlayer::InvalidMedia);
515 return;
516 }
517
518 mediaStatusChanged(QMediaPlayer::LoadingMedia);
519 applyAudioOutput();
520}
521
523{
524 if (!m_mediaEngine)
525 return;
526
527 if (mediaStatus() == QMediaPlayer::NoMedia || mediaStatus() == QMediaPlayer::InvalidMedia) {
528 return;
529 }
530
531 if (mediaStatus() == QMediaPlayer::LoadingMedia) {
532 m_pendingPlay = true;
533 return;
534 }
535
536 if (mediaStatus() == QMediaPlayer::EndOfMedia) {
537 if (!m_seekTarget) {
538 // Play() on an ended engine restarts from the beginning (HTML5 semantics).
539 // Seeking explicitly before Play() makes the engine report ENDED again.
540 if (!m_mediaEngine->IsEnded())
541 m_mediaEngine->SetCurrentTime(0);
542 positionChanged(0);
543 }
544 resetCurrentLoop();
545 mediaStatusChanged(QMediaPlayer::BufferingMedia);
546 } else if (mediaStatus() == QMediaPlayer::LoadedMedia
547 || mediaStatus() == QMediaPlayer::BufferedMedia) {
548 mediaStatusChanged(QMediaPlayer::BufferingMedia);
549 }
550
551 HRESULT hr = m_mediaEngine->Play();
552 if (FAILED(hr)) {
553 qCWarning(lcMediaEngine) << "Play:" << QSystemError::windowsComString(hr);
554 return;
555 }
556
557 stateChanged(QMediaPlayer::PlayingState);
558 startPlaybackTimers();
559}
560
562{
563 if (!m_mediaEngine)
564 return;
565
566 m_pendingPlay = false;
567
568 // Like play(), pause() from EndOfMedia rewinds to the beginning.
569 const bool fromEndOfMedia = mediaStatus() == QMediaPlayer::EndOfMedia;
570 if (fromEndOfMedia) {
571 if (!m_seekTarget)
572 setPosition(0);
573 resetCurrentLoop();
574 }
575
576 HRESULT hr = m_mediaEngine->Pause();
577 if (FAILED(hr)) {
578 qCWarning(lcMediaEngine) << "Pause:" << QSystemError::windowsComString(hr);
579 return;
580 }
581
582 if (fromEndOfMedia)
583 mediaStatusChanged(QMediaPlayer::BufferedMedia);
584
585 if (mediaStatus() != QMediaPlayer::NoMedia && mediaStatus() != QMediaPlayer::InvalidMedia) {
586 stateChanged(QMediaPlayer::PausedState);
587 m_positionUpdateTimer.stop();
588 }
589
590 // Briefly start the render timer to deliver the current frame.
591 if (isVideoAvailable() && !m_videoRenderTimer.isActive())
592 m_videoRenderTimer.start();
593}
594
596{
597 if (!m_mediaEngine)
598 return;
599
600 m_pendingPlay = false;
601 m_seekTarget.reset();
602 m_seekTimeoutTimer.stop();
603 stopPlaybackTimers();
604
605 // Don't call Pause() if already at EndOfMedia — the engine is already
606 // stopped internally, and Pause() in this state can prevent subsequent
607 // Play() from working.
608 if (mediaStatus() != QMediaPlayer::EndOfMedia) {
609 HRESULT hr = m_mediaEngine->Pause();
610 if (FAILED(hr))
611 qCWarning(lcMediaEngine) << "Pause (in stop):" << QSystemError::windowsComString(hr);
612 }
613
614 // Flush the current position before SetCurrentTime(0) so that
615 // QPlatformMediaPlayer's duplicate filter doesn't suppress positionChanged(0).
616 positionChanged(currentPositionMs());
617
618 m_mediaEngine->SetCurrentTime(0);
619
620 if (m_videoSink)
621 m_videoSink->setVideoFrame({});
622
623 stateChanged(QMediaPlayer::StoppedState);
624
625 positionChanged(0);
626
627 if (mediaStatus() == QMediaPlayer::BufferedMedia
628 || mediaStatus() == QMediaPlayer::BufferingMedia
629 || mediaStatus() == QMediaPlayer::EndOfMedia) {
630 mediaStatusChanged(QMediaPlayer::LoadedMedia);
631 bufferProgressChanged(0);
632 }
633
634 resetCurrentLoop();
635}
636
638{
639 if (!m_mediaEngine)
640 return 0;
641
642 if (m_seekTarget.has_value())
643 return m_seekTarget->count();
644
645 return currentPositionMs();
646}
647
648qint64 QMediaEnginePlayer::currentPositionMs() const
649{
650 if (!m_mediaEngine)
651 return 0;
652 const qint64 pos = qRound64(m_mediaEngine->GetCurrentTime() * 1000.0);
653 // GetCurrentTime() can overshoot the reported duration slightly.
654 return m_duration.count() > 0 ? qMin(pos, m_duration.count()) : pos;
655}
656
657QSize QMediaEnginePlayer::nativeVideoSize() const
658{
659 if (!m_mediaEngine)
660 return {};
661 DWORD w = 0, h = 0;
662 m_mediaEngine->GetNativeVideoSize(&w, &h);
663 return { static_cast<int>(w), static_cast<int>(h) };
664}
665
666void QMediaEnginePlayer::setPosition(qint64 position)
667{
668 if (!m_mediaEngine)
669 return;
670
671 position = qMax(qint64(0), position);
672 if (const qint64 dur = duration(); dur > 0)
673 position = qMin(position, dur);
674
675 // IMFMediaEngine auto-resumes after a seek from the ended state
676 // (per HTML5 spec). Remember the ended state before SetCurrentTime
677 // resets it, so we can pause immediately after to prevent silent
678 // playback when our state is Stopped or Paused.
679 const bool wasEnded = m_mediaEngine->IsEnded();
680
681 m_seekTarget = std::chrono::milliseconds(position);
682 m_mediaEngine->SetCurrentTime(position / 1000.0);
683
684 if (wasEnded && state() != QMediaPlayer::PlayingState)
685 m_mediaEngine->Pause();
686
687 // SEEKED may not arrive after device lost or rapid seeks.
688 // Reset m_seekTarget on timeout to unblock position/frame updates.
689 m_seekTimeoutTimer.start();
690
691 if (mediaStatus() == QMediaPlayer::EndOfMedia)
692 mediaStatusChanged(QMediaPlayer::LoadedMedia);
693
694 positionChanged(position);
695
696 if (isVideoAvailable() && state() == QMediaPlayer::PausedState)
697 m_videoRenderTimer.start();
698}
699
701{
702 return m_playbackRate;
703}
704
706{
707 if (qFuzzyCompare(rate, m_playbackRate))
708 return;
709
710 m_playbackRate = rate;
711
712 if (m_mediaEngine) {
713 m_mediaEngine->SetDefaultPlaybackRate(rate);
714 m_mediaEngine->SetPlaybackRate(rate);
715 }
716
717 playbackRateChanged(rate);
718}
719
721{
722 if (!m_mediaEngine)
723 return 0;
724
725 ComPtr<IMFMediaTimeRange> buffered;
726 if (FAILED(m_mediaEngine->GetBuffered(&buffered)) || !buffered)
727 return 0;
728
729 const DWORD count = buffered->GetLength();
730 if (count == 0)
731 return 0;
732
733 const double dur = m_mediaEngine->GetDuration();
734 if (!std::isfinite(dur) || dur <= 0)
735 return 0;
736
737 double end = 0;
738 buffered->GetEnd(count - 1, &end);
739 return static_cast<float>(qBound(0.0, end / dur, 1.0));
740}
741
743{
744 QMediaTimeRange ranges;
745 if (!m_mediaEngine)
746 return ranges;
747
748 ComPtr<IMFMediaTimeRange> buffered;
749 if (FAILED(m_mediaEngine->GetBuffered(&buffered)) || !buffered)
750 return ranges;
751
752 const DWORD count = buffered->GetLength();
753
754 for (DWORD i = 0; i < count; ++i) {
755 double start = 0, end = 0;
756 buffered->GetStart(i, &start);
757 buffered->GetEnd(i, &end);
758 ranges.addInterval(qRound64(start * 1000), qRound64(end * 1000));
759 }
760
761 return ranges;
762}
763
765{
766 return m_duration.count();
767}
768
770{
771 return m_url;
772}
773
775{
776 return m_stream;
777}
778
781{
782 return PitchCompensationAvailability::AlwaysOn;
783}
784
786{
787 return m_metaData;
788}
789
790void QMediaEnginePlayer::presentVideoFrame()
791{
792 if (!m_mediaEngine || !isVideoAvailable())
793 return;
794
795 // Auto-stop the render timer when not in playing state, regardless of
796 // which code path we take (including early returns). Keep it running
797 // while a seek is pending so we can deliver the post-seek frame.
798 const bool isPlaying = (state() == QMediaPlayer::PlayingState);
799 auto stopGuard = qScopeGuard([&] {
800 if (!isPlaying && !m_seekTarget)
801 m_videoRenderTimer.stop();
802 });
803
804 LONGLONG pts = 0;
805 if (m_mediaEngine->OnVideoStreamTick(&pts) != S_OK)
806 return;
807
808 // While a seek is pending, skip frame delivery — the engine may
809 // still return a frame from the pre-seek position.
810 if (m_seekTarget)
811 return;
812
813 if (!m_videoSink)
814 return;
815
816 const QSize videoSize = nativeVideoSize();
817 if (videoSize.isEmpty())
818 return;
819
820 if (videoSize != m_videoSize) {
821 m_videoSize = videoSize;
822 m_transferTexture.Reset();
823 }
824
825 if (!m_transferTexture) {
826 D3D11_TEXTURE2D_DESC desc = {};
827 desc.Width = videoSize.width();
828 desc.Height = videoSize.height();
829 desc.MipLevels = 1;
830 desc.ArraySize = 1;
831 desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
832 desc.SampleDesc.Count = 1;
833 desc.Usage = D3D11_USAGE_DEFAULT;
834 desc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
835 desc.MiscFlags = 0;
836
837 HRESULT hr = m_d3dDevice->CreateTexture2D(&desc, nullptr, &m_transferTexture);
838 if (FAILED(hr)) {
839 qCWarning(lcMediaEngine) << "CreateTexture2D:" << QSystemError::windowsComString(hr);
840 return;
841 }
842
843 updateVideoFrameFormat();
844 }
845
846 MFVideoNormalizedRect srcRect = { 0.0f, 0.0f, 1.0f, 1.0f };
847 RECT destRect = { 0, 0, videoSize.width(), videoSize.height() };
848
849 HRESULT hr = m_mediaEngine->TransferVideoFrame(m_transferTexture.Get(), &srcRect, &destRect,
850 nullptr);
851
852 if (FAILED(hr)) {
853 if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET) {
854
855 if (m_deviceLostRetries >= kMaxDeviceLostRetries) {
856 qCWarning(lcMediaEngine) << "Device lost recovery failed after"
857 << m_deviceLostRetries << "attempts, giving up";
858 error(QMediaPlayer::ResourceError, u"D3D11 device lost recovery failed"_s);
859 return;
860 }
861 ++m_deviceLostRetries;
862
863 qCWarning(lcMediaEngine) << "Device lost during TransferVideoFrame, recovering"
864 << "(attempt" << m_deviceLostRetries << ")...";
865
866 const qint64 savedPosition = currentPositionMs();
867 const auto savedState = state();
868 stopPlaybackTimers();
869
870 // Release dead resources immediately — the device is already lost.
871 releaseMediaEngine();
872 m_transferTexture.Reset();
873 m_d3dDevice.Reset();
874
875 // Defer re-creation to avoid re-entering presentVideoFrame().
876 QTimer::singleShot(0, context(), [this, savedPosition, savedState] {
877 if (!initD3D11()) {
878 error(QMediaPlayer::ResourceError, u"D3D11 device recreation failed"_s);
879 return;
880 }
881
882 if (!m_url.isEmpty() || m_stream) {
883 setMedia(m_url, m_stream);
884
885 if (savedPosition > 0)
886 setPosition(savedPosition);
887 if (savedState == QMediaPlayer::PlayingState)
888 play();
889 }
890 });
891 return;
892 }
893 qCWarning(lcMediaEngine) << "TransferVideoFrame:" << QSystemError::windowsComString(hr);
894 return;
895 }
896
897 // Successful frame transfer — reset device lost counter.
898 m_deviceLostRetries = 0;
899
900 auto buffer = std::make_unique<QMediaEngineVideoBuffer>(m_transferTexture, m_d3dDevice,
901 videoSize, m_textureImporter);
902
903 QVideoFrame frame = QVideoFramePrivate::createFrame(std::move(buffer), m_videoFormat);
904
905 auto startTime = std::chrono::round<std::chrono::microseconds>(reference_time(pts));
906
907 frame.setStartTime(startTime.count());
908 if (m_frameDuration > 0us)
909 frame.setEndTime((startTime + m_frameDuration).count());
910
911 m_videoSink->setVideoFrame(frame);
912 m_videoSink->setNativeSize(videoSize);
913}
914
915// IMFMediaEngine already applies rotation to the decoded pixels, so
916// the video format does not include rotation — VideoOutput would apply
917// it a second time.
918void QMediaEnginePlayer::updateVideoFrameFormat()
919{
920 const QSize size = nativeVideoSize();
921 if (size.isEmpty())
922 return;
923
924 m_videoFormat = QVideoFrameFormat(size, QVideoFrameFormat::Format_BGRA8888);
925 m_frameDuration = videoFrameDurationUs(m_mediaEngineEx.Get());
926}
927
928void QMediaEnginePlayer::updateNativeSize()
929{
930 if (!m_videoSink)
931 return;
932
933 const QSize size = nativeVideoSize();
934 if (!size.isEmpty())
935 m_videoSink->setNativeSize(size);
936}
937
938void QMediaEnginePlayer::setVideoSink(QVideoSink *sink)
939{
940 auto *newSink = sink ? sink->platformVideoSink() : nullptr;
941 auto *oldSink = m_videoSink;
942 m_videoSink = newSink;
943 updateNativeSize();
944
945 // Transfer the current frame to the new sink (e.g. when switching
946 // sinks while paused — OnVideoStreamTick won't return the same frame).
947 if (newSink && oldSink)
948 newSink->setVideoFrame(oldSink->currentVideoFrame());
949}
950
951void QMediaEnginePlayer::setAudioOutput(QPlatformAudioOutput *output)
952{
953 if (m_audioOutput == output)
954 return;
955
956 if (m_audioOutput && m_audioOutput->q)
957 m_audioOutput->q->disconnect(context());
958
959 m_audioOutput = output;
960
961 if (output && output->q) {
962 QObject::connect(output->q, &QAudioOutput::volumeChanged, context(), [this] {
963 applyVolume();
964 });
965 QObject::connect(output->q, &QAudioOutput::mutedChanged, context(), [this] {
966 applyVolume();
967 });
968 QObject::connect(output->q, &QAudioOutput::deviceChanged, context(), [this] {
969 applyAudioDevice();
970 });
971 }
972
973 applyAudioOutput();
974}
975
976void QMediaEnginePlayer::applyAudioOutput()
977{
978 applyVolume();
979 applyAudioDevice();
980}
981
982void QMediaEnginePlayer::applyVolume()
983{
984 if (!m_mediaEngine)
985 return;
986
987 if (!m_audioOutput) {
988 m_mediaEngine->SetMuted(TRUE);
989 return;
990 }
991
992 m_mediaEngine->SetVolume(static_cast<double>(m_audioOutput->volume));
993 m_mediaEngine->SetMuted(m_audioOutput->muted ? TRUE : FALSE);
994}
995
996// SetAudioEndpointId() reconfigures the audio renderer even if the endpoint does not change, so
997// it must only be called when the device or the engine changes, not e.g. on volume changes.
998void QMediaEnginePlayer::applyAudioDevice()
999{
1000 if (!m_mediaEngine || !m_audioOutput)
1001 return;
1002
1003 const QByteArray id = m_audioOutput->device.id();
1004 if (id.isEmpty())
1005 return;
1006
1007 ComPtr<IMFMediaEngineAudioEndpointId> endpointId;
1008 if (FAILED(m_mediaEngine.As(&endpointId)))
1009 return;
1010
1011 const QString deviceId = QString::fromUtf8(id);
1012 const HRESULT hr = endpointId->SetAudioEndpointId(reinterpret_cast<LPCWSTR>(deviceId.utf16()));
1013 if (FAILED(hr))
1014 qCWarning(lcMediaEngine) << "SetAudioEndpointId:" << QSystemError::windowsComString(hr);
1015}
1016
1017void QMediaEnginePlayer::updateMetaData()
1018{
1019 if (!m_mediaEngineEx)
1020 return;
1021
1022 m_metaData = {};
1023
1024 const QSize size = nativeVideoSize();
1025 if (!size.isEmpty())
1026 m_metaData.insert(QMediaMetaData::Resolution, size);
1027
1028 if (m_duration > 0ms)
1029 m_metaData.insert(QMediaMetaData::Duration, m_duration.count());
1030
1031 m_metadataFuture.cancel();
1032
1033 const QUrl url = m_url;
1034
1035 // IMFSourceResolver needs a URL; skip for pure QIODevice streams.
1036 if (url.isEmpty()) {
1037 metaDataChanged();
1038 return;
1039 }
1040
1041 m_metadataFuture = QtConcurrent::run([url]() -> QMediaMetaData {
1042 ComPtr<IMFSourceResolver> resolver;
1043 if (FAILED(MFCreateSourceResolver(&resolver)))
1044 return {};
1045
1046 ComPtr<IUnknown> sourceUnk;
1047 MF_OBJECT_TYPE objectType = MF_OBJECT_INVALID;
1048 HRESULT hr = E_FAIL;
1049
1050 if (url.scheme() == u"qrc"_s) {
1051 auto file = std::make_unique<QFile>(u':' + url.path());
1052 if (!file->open(QIODevice::ReadOnly))
1053 return {};
1054
1055 // MFStream in synchronous mode: BeginRead performs the read
1056 // immediately and invokes the callback on the calling thread,
1057 // so no event loop is needed. ownStream=true so MFStream
1058 // takes ownership of the QFile.
1059 ComPtr<MFStream> mfStream =
1060 makeComObject<MFStream>(file.release(), true, true /*synchronous*/);
1061
1062 const auto urlStr = url.toString();
1063 hr = resolver->CreateObjectFromByteStream(
1064 mfStream.Get(), reinterpret_cast<LPCWSTR>(urlStr.utf16()),
1065 MF_RESOLUTION_MEDIASOURCE
1066 | MF_RESOLUTION_CONTENT_DOES_NOT_HAVE_TO_MATCH_EXTENSION_OR_MIME_TYPE,
1067 nullptr, &objectType, &sourceUnk);
1068 } else {
1069 const auto urlStr = url.toString();
1070 hr = resolver->CreateObjectFromURL(
1071 reinterpret_cast<LPCWSTR>(urlStr.utf16()),
1072 MF_RESOLUTION_MEDIASOURCE
1073 | MF_RESOLUTION_CONTENT_DOES_NOT_HAVE_TO_MATCH_EXTENSION_OR_MIME_TYPE,
1074 nullptr, &objectType, &sourceUnk);
1075 }
1076
1077 if (FAILED(hr) || !sourceUnk)
1078 return {};
1079
1080 ComPtr<IMFMediaSource> mediaSource;
1081 if (FAILED(sourceUnk.As(&mediaSource)))
1082 return {};
1083
1084 QMediaMetaData result = MFMetaData::fromNative(mediaSource.Get());
1085 mediaSource->Shutdown();
1086 return result;
1087 }).then(context(), [this](QMediaMetaData rich) {
1088 if (!rich.isEmpty()) {
1089 for (const auto &[key, value] : std::as_const(m_metaData).asKeyValueRange())
1090 rich.insert(key, value);
1091 m_metaData = rich;
1092 }
1093 metaDataChanged();
1094 });
1095}
1096
1097void QMediaEnginePlayer::updateTracks()
1098{
1099 if (!m_mediaEngineEx)
1100 return;
1101
1102 m_tracks = {};
1103
1104 DWORD streamCount = 0;
1105 if (FAILED(m_mediaEngineEx->GetNumberOfStreams(&streamCount)))
1106 return;
1107
1108 for (DWORD i = 0; i < streamCount; ++i) {
1109 std::optional<TrackType> trackType;
1110 {
1111 ScopedPropVariant var;
1112 if (FAILED(m_mediaEngineEx->GetStreamAttribute(i, MF_MT_MAJOR_TYPE, var.get())))
1113 continue;
1114
1115 if (var->vt == VT_CLSID) {
1116 if (*var->puuid == MFMediaType_Audio)
1117 trackType = TrackType::AudioStream;
1118 else if (*var->puuid == MFMediaType_Video)
1119 trackType = TrackType::VideoStream;
1120 }
1121 }
1122
1123 if (!trackType)
1124 continue;
1125
1126 QMediaMetaData trackMeta;
1127 {
1128 ScopedPropVariant var;
1129 if (SUCCEEDED(m_mediaEngineEx->GetStreamAttribute(i, MF_SD_LANGUAGE, var.get()))) {
1130 if (var->vt == VT_LPWSTR && var->pwszVal)
1131 trackMeta.insert(QMediaMetaData::Language,
1132 languageVariant(QString::fromWCharArray(var->pwszVal)));
1133 }
1134 }
1135
1136 m_tracks[*trackType].entries.push_back({ trackMeta, i });
1137
1138 BOOL selected = FALSE;
1139 if (SUCCEEDED(m_mediaEngineEx->GetStreamSelection(i, &selected)) && selected)
1140 m_tracks[*trackType].currentIndex =
1141 static_cast<int>(m_tracks[*trackType].entries.size()) - 1;
1142 }
1143
1144 updateSubtitleTracks();
1145
1146 tracksChanged();
1147}
1148
1149void QMediaEnginePlayer::updateSubtitleTracks()
1150{
1151 auto &subtitleInfo = m_tracks[TrackType::SubtitleStream];
1152 subtitleInfo = {};
1153
1154 if (!m_timedText)
1155 return;
1156
1157 ComPtr<IMFTimedTextTrackList> trackList;
1158 if (FAILED(m_timedText->GetTextTracks(&trackList)) || !trackList)
1159 return;
1160
1161 const DWORD count = trackList->GetLength();
1162 for (DWORD i = 0; i < count; ++i) {
1163 ComPtr<IMFTimedTextTrack> track;
1164 if (FAILED(trackList->GetTrack(i, &track)) || !track)
1165 continue;
1166
1167 QMediaMetaData trackMeta;
1168
1169 QComTaskResource<WCHAR> label;
1170 if (SUCCEEDED(track->GetLabel(label.address())) && label)
1171 trackMeta.insert(QMediaMetaData::Title, QString::fromWCharArray(label.get()));
1172
1173 QComTaskResource<WCHAR> lang;
1174 if (SUCCEEDED(track->GetLanguage(lang.address())) && lang)
1175 trackMeta.insert(QMediaMetaData::Language,
1176 languageVariant(QString::fromWCharArray(lang.get())));
1177
1178 // Store the timed-text track ID as the streamIndex for SelectTrack.
1179 const DWORD trackId = track->GetId();
1180
1181 subtitleInfo.entries.push_back({ trackMeta, trackId });
1182
1183 if (track->IsActive()) {
1184 subtitleInfo.currentIndex = static_cast<int>(subtitleInfo.entries.size()) - 1;
1185 }
1186 }
1187}
1188
1189void QMediaEnginePlayer::updateDuration()
1190{
1191 if (!m_mediaEngine)
1192 return;
1193
1194 const double dur = m_mediaEngine->GetDuration();
1195 const auto newDuration = std::chrono::milliseconds(
1196 (std::isfinite(dur) && dur >= 0) ? qRound64(dur * 1000.0) : 0);
1197 if (newDuration != m_duration) {
1198 m_duration = newDuration;
1199 durationChanged(m_duration.count());
1200 }
1201}
1202
1203int QMediaEnginePlayer::trackCount(TrackType type)
1204{
1205 return static_cast<int>(m_tracks[type].entries.size());
1206}
1207
1208QMediaMetaData QMediaEnginePlayer::trackMetaData(TrackType type, int trackNumber)
1209{
1210 if (trackNumber < 0 || trackNumber >= static_cast<int>(m_tracks[type].entries.size()))
1211 return {};
1212 return m_tracks[type].entries.at(trackNumber).metaData;
1213}
1214
1216{
1217 return m_tracks[type].currentIndex;
1218}
1219
1220void QMediaEnginePlayer::setActiveTrack(TrackType type, int index)
1221{
1222 if (index < -1 || index >= static_cast<int>(m_tracks[type].entries.size()))
1223 return;
1224
1225 if (type == TrackType::SubtitleStream) {
1226 // Subtitle tracks are managed via IMFTimedText, not stream selection.
1227 if (!m_timedText)
1228 return;
1229
1230 const int currentIdx = m_tracks[type].currentIndex;
1231 if (currentIdx >= 0 && currentIdx < static_cast<int>(m_tracks[type].entries.size())) {
1232 DWORD trackId = m_tracks[type].entries.at(currentIdx).streamIndex;
1233 m_timedText->SelectTrack(trackId, FALSE);
1234 }
1235
1236 if (index >= 0) {
1237 DWORD trackId = m_tracks[type].entries.at(index).streamIndex;
1238 m_timedText->SelectTrack(trackId, TRUE);
1239 }
1240
1241 if (m_videoSink)
1242 m_videoSink->setSubtitleText({});
1243
1244 m_tracks[type].currentIndex = index;
1245 activeTracksChanged();
1246 return;
1247 }
1248
1249 if (!m_mediaEngineEx)
1250 return;
1251
1252 const int currentIdx = m_tracks[type].currentIndex;
1253 if (currentIdx >= 0 && currentIdx < static_cast<int>(m_tracks[type].entries.size())) {
1254 DWORD streamIdx = m_tracks[type].entries.at(currentIdx).streamIndex;
1255 m_mediaEngineEx->SetStreamSelection(streamIdx, FALSE);
1256 }
1257
1258 if (index >= 0) {
1259 DWORD streamIdx = m_tracks[type].entries.at(index).streamIndex;
1260 m_mediaEngineEx->SetStreamSelection(streamIdx, TRUE);
1261 }
1262
1263 m_tracks[type].currentIndex = index;
1264 activeTracksChanged();
1265}
1266
1267QT_END_NAMESPACE
float bufferProgress() const override
void setPlaybackRate(qreal rate) override
void setActiveTrack(TrackType type, int index) override
void setAudioOutput(QPlatformAudioOutput *output) override
qint64 duration() const override
void setMedia(const QUrl &media, QIODevice *stream) override
QMediaTimeRange availablePlaybackRanges() const override
qint64 position() const override
qreal playbackRate() const override
int activeTrack(TrackType type) override
QMediaEnginePlayer(QMediaPlayer *player)
void setVideoSink(QVideoSink *sink) override
QUrl media() const override
void setPosition(qint64 position) override
QMediaMetaData trackMetaData(TrackType type, int trackNumber) override
PitchCompensationAvailability pitchCompensationAvailability() const override
const QIODevice * mediaStream() const override
QMediaMetaData metaData() const override
int trackCount(TrackType type) override
Q_LOGGING_CATEGORY(lcEventDispatcher, "qt.eventdispatcher")