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 if (state() != QMediaPlayer::StoppedState && state() != QMediaPlayer::PausedState) {
337 stateChanged(QMediaPlayer::PausedState);
338 stopPlaybackTimers();
339 }
340 break;
341
342 case MF_MEDIA_ENGINE_EVENT_ENDED: {
343 qCDebug(lcMediaEngine) << "ENDED";
344 stopPlaybackTimers();
345
346 if (mediaStatus() == QMediaPlayer::BufferingMedia)
347 mediaStatusChanged(QMediaPlayer::BufferedMedia);
348
349 if (doLoop()) {
350 m_mediaEngine->SetCurrentTime(0);
351 m_mediaEngine->Play();
352 } else {
353 if (m_videoSink)
354 m_videoSink->setVideoFrame({});
355 positionChanged(m_duration.count());
356 stateChanged(QMediaPlayer::StoppedState);
357 mediaStatusChanged(QMediaPlayer::EndOfMedia);
358 resetCurrentLoop();
359 }
360 break;
361 }
362
363 case MF_MEDIA_ENGINE_EVENT_TIMEUPDATE:
364 if (!m_seekTarget)
365 positionChanged(currentPositionMs());
366 break;
367
368 case MF_MEDIA_ENGINE_EVENT_ERROR: {
369 const auto mfErr = static_cast<MF_MEDIA_ENGINE_ERR>(param1);
370
371 QString detail;
372 switch (mfErr) {
373 case MF_MEDIA_ENGINE_ERR_ABORTED:
374 detail = u"Playback aborted"_s;
375 break;
376 case MF_MEDIA_ENGINE_ERR_NETWORK:
377 detail = u"Network error"_s;
378 break;
379 case MF_MEDIA_ENGINE_ERR_DECODE:
380 detail = u"Unsupported or corrupt media"_s;
381 break;
382 case MF_MEDIA_ENGINE_ERR_SRC_NOT_SUPPORTED:
383 detail = u"Source not supported"_s;
384 break;
385 case MF_MEDIA_ENGINE_ERR_ENCRYPTED:
386 detail = u"Encrypted content"_s;
387 break;
388 default:
389 detail = u"Unknown error %1"_s.arg(param1);
390 break;
391 }
392
393 qCWarning(lcMediaEngine) << "Error:" << detail;
394 error(mapMediaEngineError(mfErr), detail);
395 mediaStatusChanged(QMediaPlayer::InvalidMedia);
396 stopPlaybackTimers();
397 break;
398 }
399
400 case MF_MEDIA_ENGINE_EVENT_BUFFERINGSTARTED:
401 qCDebug(lcMediaEngine) << "BUFFERINGSTARTED";
402 mediaStatusChanged(QMediaPlayer::BufferingMedia);
403 bufferProgressChanged(bufferProgress());
404 break;
405
406 case MF_MEDIA_ENGINE_EVENT_BUFFERINGENDED:
407 qCDebug(lcMediaEngine) << "BUFFERINGENDED";
408 mediaStatusChanged(QMediaPlayer::BufferedMedia);
409 bufferProgressChanged(bufferProgress());
410 break;
411
412 case MF_MEDIA_ENGINE_EVENT_DURATIONCHANGE:
413 qCDebug(lcMediaEngine) << "DURATIONCHANGE";
414 updateDuration();
415 break;
416
417 case MF_MEDIA_ENGINE_EVENT_FORMATCHANGE:
418 qCDebug(lcMediaEngine) << "FORMATCHANGE";
419 updateVideoFrameFormat();
420 break;
421
422 case MF_MEDIA_ENGINE_EVENT_FIRSTFRAMEREADY:
423 qCDebug(lcMediaEngine) << "FIRSTFRAMEREADY";
424 if (state() != QMediaPlayer::StoppedState)
425 presentVideoFrame();
426 break;
427
428 case MF_MEDIA_ENGINE_EVENT_SEEKED:
429 qCDebug(lcMediaEngine) << "SEEKED";
430 m_seekTimeoutTimer.stop();
431 m_seekTarget.reset();
432 break;
433
434 default:
435 break;
436 }
437}
438
439void QMediaEnginePlayer::setMedia(const QUrl &media, QIODevice *stream)
440{
441 qCDebug(lcMediaEngine) << "setMedia:" << media << "stream:" << stream;
442
443 if (state() != QMediaPlayer::StoppedState)
444 stop();
445
446 m_url = media;
447 m_stream = stream;
448
449 m_duration = {};
450 m_pendingPlay = false;
451 m_seekTarget.reset();
452 m_seekTimeoutTimer.stop();
453 seekableChanged(false);
454 m_videoSize = {};
455 m_transferTexture.Reset();
456 m_metaData = {};
457 m_metadataFuture.cancel();
458
459 if (m_videoSink)
460 m_videoSink->setVideoFrame({});
461
462 m_tracks = {};
463
464 if (media.isEmpty() && !stream) {
465 audioAvailableChanged(false);
466 videoAvailableChanged(false);
467 // IMFMediaEngine has no API to clear the current source without
468 // shutting down. Release the engine now; it will be lazily
469 // recreated on the next setMedia() call with a real source.
470 releaseMediaEngine();
471 metaDataChanged();
472 mediaStatusChanged(QMediaPlayer::NoMedia);
473 return;
474 }
475
476 // Lazy init: create the MediaEngine on first use,
477 // or recreate it after a previous NoMedia release.
478 if (!m_mediaEngine) {
479 if (!initMediaEngine()) {
480 error(QMediaPlayer::ResourceError, u"MediaEngine not initialized"_s);
481 mediaStatusChanged(QMediaPlayer::InvalidMedia);
482 return;
483 }
484 }
485
486 HRESULT hr = E_FAIL;
487
488 if (stream) {
489 ComPtr<MFStream> mfStream = makeComObject<MFStream>(stream, false);
490 QString urlStr = media.toString();
491 // SetSourceFromByteStream requires a non-null URL.
492 // Use the real file name when available so that Media Foundation
493 // can infer the MIME type from the extension.
494 if (urlStr.isEmpty()) {
495 if (auto *file = qobject_cast<QFile *>(stream))
496 urlStr = u"file:///"_s + file->fileName();
497 else
498 urlStr = u"stream:///media"_s;
499 }
500 const QBStr bstrUrl(urlStr);
501 hr = m_mediaEngineEx->SetSourceFromByteStream(mfStream.Get(), bstrUrl.bstr());
502 } else {
503 const QBStr bstrUrl(media.toString());
504 hr = m_mediaEngine->SetSource(bstrUrl.bstr());
505 }
506
507 if (FAILED(hr)) {
508 qCWarning(lcMediaEngine) << "SetSource:" << QSystemError::windowsComString(hr);
509 error(QMediaPlayer::ResourceError, u"Failed to set media source"_s);
510 mediaStatusChanged(QMediaPlayer::InvalidMedia);
511 return;
512 }
513
514 mediaStatusChanged(QMediaPlayer::LoadingMedia);
515 applyAudioOutput();
516}
517
519{
520 if (!m_mediaEngine)
521 return;
522
523 if (mediaStatus() == QMediaPlayer::NoMedia || mediaStatus() == QMediaPlayer::InvalidMedia) {
524 return;
525 }
526
527 if (mediaStatus() == QMediaPlayer::LoadingMedia) {
528 m_pendingPlay = true;
529 return;
530 }
531
532 if (mediaStatus() == QMediaPlayer::EndOfMedia) {
533 if (!m_seekTarget) {
534 m_mediaEngine->SetCurrentTime(0);
535 positionChanged(0);
536 }
537 resetCurrentLoop();
538 mediaStatusChanged(QMediaPlayer::BufferingMedia);
539 } else if (mediaStatus() == QMediaPlayer::LoadedMedia
540 || mediaStatus() == QMediaPlayer::BufferedMedia) {
541 mediaStatusChanged(QMediaPlayer::BufferingMedia);
542 }
543
544 HRESULT hr = m_mediaEngine->Play();
545 if (FAILED(hr)) {
546 qCWarning(lcMediaEngine) << "Play:" << QSystemError::windowsComString(hr);
547 return;
548 }
549
550 stateChanged(QMediaPlayer::PlayingState);
551 startPlaybackTimers();
552}
553
555{
556 if (!m_mediaEngine)
557 return;
558
559 m_pendingPlay = false;
560 HRESULT hr = m_mediaEngine->Pause();
561 if (FAILED(hr)) {
562 qCWarning(lcMediaEngine) << "Pause:" << QSystemError::windowsComString(hr);
563 return;
564 }
565
566 if (mediaStatus() != QMediaPlayer::NoMedia && mediaStatus() != QMediaPlayer::InvalidMedia) {
567 stateChanged(QMediaPlayer::PausedState);
568 m_positionUpdateTimer.stop();
569 }
570
571 // Briefly start the render timer to deliver the current frame.
572 if (isVideoAvailable() && !m_videoRenderTimer.isActive())
573 m_videoRenderTimer.start();
574}
575
577{
578 if (!m_mediaEngine)
579 return;
580
581 m_pendingPlay = false;
582 m_seekTarget.reset();
583 m_seekTimeoutTimer.stop();
584 stopPlaybackTimers();
585
586 // Don't call Pause() if already at EndOfMedia — the engine is already
587 // stopped internally, and Pause() in this state can prevent subsequent
588 // Play() from working.
589 if (mediaStatus() != QMediaPlayer::EndOfMedia) {
590 HRESULT hr = m_mediaEngine->Pause();
591 if (FAILED(hr))
592 qCWarning(lcMediaEngine) << "Pause (in stop):" << QSystemError::windowsComString(hr);
593 }
594
595 // Flush the current position before SetCurrentTime(0) so that
596 // QPlatformMediaPlayer's duplicate filter doesn't suppress positionChanged(0).
597 positionChanged(currentPositionMs());
598
599 m_mediaEngine->SetCurrentTime(0);
600
601 if (m_videoSink)
602 m_videoSink->setVideoFrame({});
603
604 stateChanged(QMediaPlayer::StoppedState);
605
606 positionChanged(0);
607
608 if (mediaStatus() == QMediaPlayer::BufferedMedia
609 || mediaStatus() == QMediaPlayer::BufferingMedia
610 || mediaStatus() == QMediaPlayer::EndOfMedia) {
611 mediaStatusChanged(QMediaPlayer::LoadedMedia);
612 bufferProgressChanged(0);
613 }
614
615 resetCurrentLoop();
616}
617
619{
620 if (!m_mediaEngine)
621 return 0;
622
623 if (m_seekTarget.has_value())
624 return m_seekTarget->count();
625
626 return currentPositionMs();
627}
628
629qint64 QMediaEnginePlayer::currentPositionMs() const
630{
631 if (!m_mediaEngine)
632 return 0;
633 const qint64 pos = qRound64(m_mediaEngine->GetCurrentTime() * 1000.0);
634 // GetCurrentTime() can overshoot the reported duration slightly.
635 return m_duration.count() > 0 ? qMin(pos, m_duration.count()) : pos;
636}
637
638QSize QMediaEnginePlayer::nativeVideoSize() const
639{
640 if (!m_mediaEngine)
641 return {};
642 DWORD w = 0, h = 0;
643 m_mediaEngine->GetNativeVideoSize(&w, &h);
644 return { static_cast<int>(w), static_cast<int>(h) };
645}
646
647void QMediaEnginePlayer::setPosition(qint64 position)
648{
649 if (!m_mediaEngine)
650 return;
651
652 position = qMax(qint64(0), position);
653 if (const qint64 dur = duration(); dur > 0)
654 position = qMin(position, dur);
655
656 // IMFMediaEngine auto-resumes after a seek from the ended state
657 // (per HTML5 spec). Remember the ended state before SetCurrentTime
658 // resets it, so we can pause immediately after to prevent silent
659 // playback when our state is Stopped or Paused.
660 const bool wasEnded = m_mediaEngine->IsEnded();
661
662 m_seekTarget = std::chrono::milliseconds(position);
663 m_mediaEngine->SetCurrentTime(position / 1000.0);
664
665 if (wasEnded && state() != QMediaPlayer::PlayingState)
666 m_mediaEngine->Pause();
667
668 // SEEKED may not arrive after device lost or rapid seeks.
669 // Reset m_seekTarget on timeout to unblock position/frame updates.
670 m_seekTimeoutTimer.start();
671
672 if (mediaStatus() == QMediaPlayer::EndOfMedia)
673 mediaStatusChanged(QMediaPlayer::LoadedMedia);
674
675 positionChanged(position);
676
677 if (isVideoAvailable() && state() == QMediaPlayer::PausedState)
678 m_videoRenderTimer.start();
679}
680
682{
683 return m_playbackRate;
684}
685
687{
688 if (qFuzzyCompare(rate, m_playbackRate))
689 return;
690
691 m_playbackRate = rate;
692
693 if (m_mediaEngine) {
694 m_mediaEngine->SetDefaultPlaybackRate(rate);
695 m_mediaEngine->SetPlaybackRate(rate);
696 }
697
698 playbackRateChanged(rate);
699}
700
702{
703 if (!m_mediaEngine)
704 return 0;
705
706 ComPtr<IMFMediaTimeRange> buffered;
707 if (FAILED(m_mediaEngine->GetBuffered(&buffered)) || !buffered)
708 return 0;
709
710 const DWORD count = buffered->GetLength();
711 if (count == 0)
712 return 0;
713
714 const double dur = m_mediaEngine->GetDuration();
715 if (!std::isfinite(dur) || dur <= 0)
716 return 0;
717
718 double end = 0;
719 buffered->GetEnd(count - 1, &end);
720 return static_cast<float>(qBound(0.0, end / dur, 1.0));
721}
722
724{
725 QMediaTimeRange ranges;
726 if (!m_mediaEngine)
727 return ranges;
728
729 ComPtr<IMFMediaTimeRange> buffered;
730 if (FAILED(m_mediaEngine->GetBuffered(&buffered)) || !buffered)
731 return ranges;
732
733 const DWORD count = buffered->GetLength();
734
735 for (DWORD i = 0; i < count; ++i) {
736 double start = 0, end = 0;
737 buffered->GetStart(i, &start);
738 buffered->GetEnd(i, &end);
739 ranges.addInterval(qRound64(start * 1000), qRound64(end * 1000));
740 }
741
742 return ranges;
743}
744
746{
747 return m_duration.count();
748}
749
751{
752 return m_url;
753}
754
756{
757 return m_stream;
758}
759
762{
763 return PitchCompensationAvailability::AlwaysOn;
764}
765
767{
768 return m_metaData;
769}
770
771void QMediaEnginePlayer::presentVideoFrame()
772{
773 if (!m_mediaEngine || !isVideoAvailable())
774 return;
775
776 // Auto-stop the render timer when not in playing state, regardless of
777 // which code path we take (including early returns). Keep it running
778 // while a seek is pending so we can deliver the post-seek frame.
779 const bool isPlaying = (state() == QMediaPlayer::PlayingState);
780 auto stopGuard = qScopeGuard([&] {
781 if (!isPlaying && !m_seekTarget)
782 m_videoRenderTimer.stop();
783 });
784
785 LONGLONG pts = 0;
786 if (m_mediaEngine->OnVideoStreamTick(&pts) != S_OK)
787 return;
788
789 // While a seek is pending, skip frame delivery — the engine may
790 // still return a frame from the pre-seek position.
791 if (m_seekTarget)
792 return;
793
794 if (!m_videoSink)
795 return;
796
797 const QSize videoSize = nativeVideoSize();
798 if (videoSize.isEmpty())
799 return;
800
801 if (videoSize != m_videoSize) {
802 m_videoSize = videoSize;
803 m_transferTexture.Reset();
804 }
805
806 if (!m_transferTexture) {
807 D3D11_TEXTURE2D_DESC desc = {};
808 desc.Width = videoSize.width();
809 desc.Height = videoSize.height();
810 desc.MipLevels = 1;
811 desc.ArraySize = 1;
812 desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
813 desc.SampleDesc.Count = 1;
814 desc.Usage = D3D11_USAGE_DEFAULT;
815 desc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
816 desc.MiscFlags = 0;
817
818 HRESULT hr = m_d3dDevice->CreateTexture2D(&desc, nullptr, &m_transferTexture);
819 if (FAILED(hr)) {
820 qCWarning(lcMediaEngine) << "CreateTexture2D:" << QSystemError::windowsComString(hr);
821 return;
822 }
823
824 updateVideoFrameFormat();
825 }
826
827 MFVideoNormalizedRect srcRect = { 0.0f, 0.0f, 1.0f, 1.0f };
828 RECT destRect = { 0, 0, videoSize.width(), videoSize.height() };
829
830 HRESULT hr = m_mediaEngine->TransferVideoFrame(m_transferTexture.Get(), &srcRect, &destRect,
831 nullptr);
832
833 if (FAILED(hr)) {
834 if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET) {
835
836 if (m_deviceLostRetries >= kMaxDeviceLostRetries) {
837 qCWarning(lcMediaEngine) << "Device lost recovery failed after"
838 << m_deviceLostRetries << "attempts, giving up";
839 error(QMediaPlayer::ResourceError, u"D3D11 device lost recovery failed"_s);
840 return;
841 }
842 ++m_deviceLostRetries;
843
844 qCWarning(lcMediaEngine) << "Device lost during TransferVideoFrame, recovering"
845 << "(attempt" << m_deviceLostRetries << ")...";
846
847 const qint64 savedPosition = currentPositionMs();
848 const auto savedState = state();
849 stopPlaybackTimers();
850
851 // Release dead resources immediately — the device is already lost.
852 releaseMediaEngine();
853 m_transferTexture.Reset();
854 m_d3dDevice.Reset();
855
856 // Defer re-creation to avoid re-entering presentVideoFrame().
857 QTimer::singleShot(0, context(), [this, savedPosition, savedState] {
858 if (!initD3D11()) {
859 error(QMediaPlayer::ResourceError, u"D3D11 device recreation failed"_s);
860 return;
861 }
862
863 if (!m_url.isEmpty() || m_stream) {
864 setMedia(m_url, m_stream);
865
866 if (savedPosition > 0)
867 setPosition(savedPosition);
868 if (savedState == QMediaPlayer::PlayingState)
869 play();
870 }
871 });
872 return;
873 }
874 qCWarning(lcMediaEngine) << "TransferVideoFrame:" << QSystemError::windowsComString(hr);
875 return;
876 }
877
878 // Successful frame transfer — reset device lost counter.
879 m_deviceLostRetries = 0;
880
881 auto buffer = std::make_unique<QMediaEngineVideoBuffer>(m_transferTexture, m_d3dDevice,
882 videoSize, m_textureImporter);
883
884 QVideoFrame frame = QVideoFramePrivate::createFrame(std::move(buffer), m_videoFormat);
885
886 auto startTime = std::chrono::round<std::chrono::microseconds>(reference_time(pts));
887
888 frame.setStartTime(startTime.count());
889 if (m_frameDuration > 0us)
890 frame.setEndTime((startTime + m_frameDuration).count());
891
892 m_videoSink->setVideoFrame(frame);
893 m_videoSink->setNativeSize(videoSize);
894}
895
896// IMFMediaEngine already applies rotation to the decoded pixels, so
897// the video format does not include rotation — VideoOutput would apply
898// it a second time.
899void QMediaEnginePlayer::updateVideoFrameFormat()
900{
901 const QSize size = nativeVideoSize();
902 if (size.isEmpty())
903 return;
904
905 m_videoFormat = QVideoFrameFormat(size, QVideoFrameFormat::Format_BGRA8888);
906 m_frameDuration = videoFrameDurationUs(m_mediaEngineEx.Get());
907}
908
909void QMediaEnginePlayer::updateNativeSize()
910{
911 if (!m_videoSink)
912 return;
913
914 const QSize size = nativeVideoSize();
915 if (!size.isEmpty())
916 m_videoSink->setNativeSize(size);
917}
918
919void QMediaEnginePlayer::setVideoSink(QVideoSink *sink)
920{
921 auto *newSink = sink ? sink->platformVideoSink() : nullptr;
922 auto *oldSink = m_videoSink;
923 m_videoSink = newSink;
924 updateNativeSize();
925
926 // Transfer the current frame to the new sink (e.g. when switching
927 // sinks while paused — OnVideoStreamTick won't return the same frame).
928 if (newSink && oldSink)
929 newSink->setVideoFrame(oldSink->currentVideoFrame());
930}
931
932void QMediaEnginePlayer::setAudioOutput(QPlatformAudioOutput *output)
933{
934 if (m_audioOutput == output)
935 return;
936
937 if (m_audioOutput && m_audioOutput->q)
938 m_audioOutput->q->disconnect(context());
939
940 m_audioOutput = output;
941
942 if (output && output->q) {
943 QObject::connect(output->q, &QAudioOutput::volumeChanged, context(), [this] {
944 applyAudioOutput();
945 });
946 QObject::connect(output->q, &QAudioOutput::mutedChanged, context(), [this] {
947 applyAudioOutput();
948 });
949 QObject::connect(output->q, &QAudioOutput::deviceChanged, context(), [this] {
950 applyAudioOutput();
951 });
952 }
953
954 applyAudioOutput();
955}
956
957void QMediaEnginePlayer::applyAudioOutput()
958{
959 if (!m_mediaEngine)
960 return;
961
962 if (!m_audioOutput) {
963 m_mediaEngine->SetMuted(TRUE);
964 return;
965 }
966
967 m_mediaEngine->SetVolume(static_cast<double>(m_audioOutput->volume));
968 m_mediaEngine->SetMuted(m_audioOutput->muted ? TRUE : FALSE);
969
970 const QByteArray id = m_audioOutput->device.id();
971 if (!id.isEmpty()) {
972 ComPtr<IMFMediaEngineAudioEndpointId> endpointId;
973 if (SUCCEEDED(m_mediaEngine.As(&endpointId))) {
974 const QString deviceId = QString::fromUtf8(id);
975 endpointId->SetAudioEndpointId(reinterpret_cast<LPCWSTR>(deviceId.utf16()));
976 }
977 }
978}
979
980void QMediaEnginePlayer::updateMetaData()
981{
982 if (!m_mediaEngineEx)
983 return;
984
985 m_metaData = {};
986
987 const QSize size = nativeVideoSize();
988 if (!size.isEmpty())
989 m_metaData.insert(QMediaMetaData::Resolution, size);
990
991 if (m_duration > 0ms)
992 m_metaData.insert(QMediaMetaData::Duration, m_duration.count());
993
994 m_metadataFuture.cancel();
995
996 const QUrl url = m_url;
997
998 // IMFSourceResolver needs a URL; skip for pure QIODevice streams.
999 if (url.isEmpty()) {
1000 metaDataChanged();
1001 return;
1002 }
1003
1004 m_metadataFuture = QtConcurrent::run([url]() -> QMediaMetaData {
1005 ComPtr<IMFSourceResolver> resolver;
1006 if (FAILED(MFCreateSourceResolver(&resolver)))
1007 return {};
1008
1009 ComPtr<IUnknown> sourceUnk;
1010 MF_OBJECT_TYPE objectType = MF_OBJECT_INVALID;
1011 HRESULT hr = E_FAIL;
1012
1013 if (url.scheme() == u"qrc"_s) {
1014 auto file = std::make_unique<QFile>(u':' + url.path());
1015 if (!file->open(QIODevice::ReadOnly))
1016 return {};
1017
1018 // MFStream in synchronous mode: BeginRead performs the read
1019 // immediately and invokes the callback on the calling thread,
1020 // so no event loop is needed. ownStream=true so MFStream
1021 // takes ownership of the QFile.
1022 ComPtr<MFStream> mfStream =
1023 makeComObject<MFStream>(file.release(), true, true /*synchronous*/);
1024
1025 const auto urlStr = url.toString();
1026 hr = resolver->CreateObjectFromByteStream(
1027 mfStream.Get(), reinterpret_cast<LPCWSTR>(urlStr.utf16()),
1028 MF_RESOLUTION_MEDIASOURCE
1029 | MF_RESOLUTION_CONTENT_DOES_NOT_HAVE_TO_MATCH_EXTENSION_OR_MIME_TYPE,
1030 nullptr, &objectType, &sourceUnk);
1031 } else {
1032 const auto urlStr = url.toString();
1033 hr = resolver->CreateObjectFromURL(
1034 reinterpret_cast<LPCWSTR>(urlStr.utf16()),
1035 MF_RESOLUTION_MEDIASOURCE
1036 | MF_RESOLUTION_CONTENT_DOES_NOT_HAVE_TO_MATCH_EXTENSION_OR_MIME_TYPE,
1037 nullptr, &objectType, &sourceUnk);
1038 }
1039
1040 if (FAILED(hr) || !sourceUnk)
1041 return {};
1042
1043 ComPtr<IMFMediaSource> mediaSource;
1044 if (FAILED(sourceUnk.As(&mediaSource)))
1045 return {};
1046
1047 QMediaMetaData result = MFMetaData::fromNative(mediaSource.Get());
1048 mediaSource->Shutdown();
1049 return result;
1050 }).then(context(), [this](QMediaMetaData rich) {
1051 if (!rich.isEmpty()) {
1052 for (const auto &[key, value] : std::as_const(m_metaData).asKeyValueRange())
1053 rich.insert(key, value);
1054 m_metaData = rich;
1055 }
1056 metaDataChanged();
1057 });
1058}
1059
1060void QMediaEnginePlayer::updateTracks()
1061{
1062 if (!m_mediaEngineEx)
1063 return;
1064
1065 m_tracks = {};
1066
1067 DWORD streamCount = 0;
1068 if (FAILED(m_mediaEngineEx->GetNumberOfStreams(&streamCount)))
1069 return;
1070
1071 for (DWORD i = 0; i < streamCount; ++i) {
1072 std::optional<TrackType> trackType;
1073 {
1074 ScopedPropVariant var;
1075 if (FAILED(m_mediaEngineEx->GetStreamAttribute(i, MF_MT_MAJOR_TYPE, var.get())))
1076 continue;
1077
1078 if (var->vt == VT_CLSID) {
1079 if (*var->puuid == MFMediaType_Audio)
1080 trackType = TrackType::AudioStream;
1081 else if (*var->puuid == MFMediaType_Video)
1082 trackType = TrackType::VideoStream;
1083 }
1084 }
1085
1086 if (!trackType)
1087 continue;
1088
1089 QMediaMetaData trackMeta;
1090 {
1091 ScopedPropVariant var;
1092 if (SUCCEEDED(m_mediaEngineEx->GetStreamAttribute(i, MF_SD_LANGUAGE, var.get()))) {
1093 if (var->vt == VT_LPWSTR && var->pwszVal)
1094 trackMeta.insert(QMediaMetaData::Language,
1095 languageVariant(QString::fromWCharArray(var->pwszVal)));
1096 }
1097 }
1098
1099 m_tracks[*trackType].entries.push_back({ trackMeta, i });
1100
1101 BOOL selected = FALSE;
1102 if (SUCCEEDED(m_mediaEngineEx->GetStreamSelection(i, &selected)) && selected)
1103 m_tracks[*trackType].currentIndex =
1104 static_cast<int>(m_tracks[*trackType].entries.size()) - 1;
1105 }
1106
1107 updateSubtitleTracks();
1108
1109 tracksChanged();
1110}
1111
1112void QMediaEnginePlayer::updateSubtitleTracks()
1113{
1114 auto &subtitleInfo = m_tracks[TrackType::SubtitleStream];
1115 subtitleInfo = {};
1116
1117 if (!m_timedText)
1118 return;
1119
1120 ComPtr<IMFTimedTextTrackList> trackList;
1121 if (FAILED(m_timedText->GetTextTracks(&trackList)) || !trackList)
1122 return;
1123
1124 const DWORD count = trackList->GetLength();
1125 for (DWORD i = 0; i < count; ++i) {
1126 ComPtr<IMFTimedTextTrack> track;
1127 if (FAILED(trackList->GetTrack(i, &track)) || !track)
1128 continue;
1129
1130 QMediaMetaData trackMeta;
1131
1132 QComTaskResource<WCHAR> label;
1133 if (SUCCEEDED(track->GetLabel(label.address())) && label)
1134 trackMeta.insert(QMediaMetaData::Title, QString::fromWCharArray(label.get()));
1135
1136 QComTaskResource<WCHAR> lang;
1137 if (SUCCEEDED(track->GetLanguage(lang.address())) && lang)
1138 trackMeta.insert(QMediaMetaData::Language,
1139 languageVariant(QString::fromWCharArray(lang.get())));
1140
1141 // Store the timed-text track ID as the streamIndex for SelectTrack.
1142 const DWORD trackId = track->GetId();
1143
1144 subtitleInfo.entries.push_back({ trackMeta, trackId });
1145
1146 if (track->IsActive()) {
1147 subtitleInfo.currentIndex = static_cast<int>(subtitleInfo.entries.size()) - 1;
1148 }
1149 }
1150}
1151
1152void QMediaEnginePlayer::updateDuration()
1153{
1154 if (!m_mediaEngine)
1155 return;
1156
1157 const double dur = m_mediaEngine->GetDuration();
1158 const auto newDuration = std::chrono::milliseconds(
1159 (std::isfinite(dur) && dur >= 0) ? qRound64(dur * 1000.0) : 0);
1160 if (newDuration != m_duration) {
1161 m_duration = newDuration;
1162 durationChanged(m_duration.count());
1163 }
1164}
1165
1166int QMediaEnginePlayer::trackCount(TrackType type)
1167{
1168 return static_cast<int>(m_tracks[type].entries.size());
1169}
1170
1171QMediaMetaData QMediaEnginePlayer::trackMetaData(TrackType type, int trackNumber)
1172{
1173 if (trackNumber < 0 || trackNumber >= static_cast<int>(m_tracks[type].entries.size()))
1174 return {};
1175 return m_tracks[type].entries.at(trackNumber).metaData;
1176}
1177
1179{
1180 return m_tracks[type].currentIndex;
1181}
1182
1183void QMediaEnginePlayer::setActiveTrack(TrackType type, int index)
1184{
1185 if (index < -1 || index >= static_cast<int>(m_tracks[type].entries.size()))
1186 return;
1187
1188 if (type == TrackType::SubtitleStream) {
1189 // Subtitle tracks are managed via IMFTimedText, not stream selection.
1190 if (!m_timedText)
1191 return;
1192
1193 const int currentIdx = m_tracks[type].currentIndex;
1194 if (currentIdx >= 0 && currentIdx < static_cast<int>(m_tracks[type].entries.size())) {
1195 DWORD trackId = m_tracks[type].entries.at(currentIdx).streamIndex;
1196 m_timedText->SelectTrack(trackId, FALSE);
1197 }
1198
1199 if (index >= 0) {
1200 DWORD trackId = m_tracks[type].entries.at(index).streamIndex;
1201 m_timedText->SelectTrack(trackId, TRUE);
1202 }
1203
1204 if (m_videoSink)
1205 m_videoSink->setSubtitleText({});
1206
1207 m_tracks[type].currentIndex = index;
1208 activeTracksChanged();
1209 return;
1210 }
1211
1212 if (!m_mediaEngineEx)
1213 return;
1214
1215 const int currentIdx = m_tracks[type].currentIndex;
1216 if (currentIdx >= 0 && currentIdx < static_cast<int>(m_tracks[type].entries.size())) {
1217 DWORD streamIdx = m_tracks[type].entries.at(currentIdx).streamIndex;
1218 m_mediaEngineEx->SetStreamSelection(streamIdx, FALSE);
1219 }
1220
1221 if (index >= 0) {
1222 DWORD streamIdx = m_tracks[type].entries.at(index).streamIndex;
1223 m_mediaEngineEx->SetStreamSelection(streamIdx, TRUE);
1224 }
1225
1226 m_tracks[type].currentIndex = index;
1227 activeTracksChanged();
1228}
1229
1230QT_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")