Revision control

Copy as Markdown

// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qmediaengineplayer_p.h"
#include <common/mfmetadata_p.h>
#include <mfstream_p.h>
#include <player/qmediaenginenotify_p.h>
#include <player/qmediaenginetimedtextnotify_p.h>
#include <player/qmediaenginevideobuffer_p.h>
#include <QtMultimedia/qaudiooutput.h>
#include <QtMultimedia/qvideosink.h>
#include <QtMultimedia/private/qcomtaskresource_p.h>
#include <QtMultimedia/private/qplatformaudiooutput_p.h>
#include <QtMultimedia/private/qplatformvideosink_p.h>
#include <QtMultimedia/private/qvideoframe_p.h>
#include <QtMultimedia/private/qwindows_scopedpropvariant_p.h>
#include <QtMultimedia/private/qwindowsd3d11rhitextureimporter_p.h>
#include <QtMultimedia/private/qwmf_support_p.h>
#include <QtConcurrent/qtconcurrentrun.h>
#include <QtCore/qfile.h>
#include <QtCore/qlocale.h>
#include <QtCore/qloggingcategory.h>
#include <QtCore/qscopeguard.h>
#include <QtCore/qtimer.h>
#include <QtCore/private/qbstr_p.h>
#include <QtCore/private/qsystemerror_p.h>
#include <dxgi.h>
#include <mfapi.h>
#include <mfidl.h>
Q_LOGGING_CATEGORY(lcMediaEngine, "qt.multimedia.mediaengine")
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
using namespace std::chrono_literals;
using QtMultimediaPrivate::ScopedPropVariant;
using reference_time = QWMF::reference_time;
namespace {
QVariant languageVariant(const QString &langStr)
{
const QLocale::Language lang = QLocale::codeToLanguage(langStr, QLocale::AnyLanguageCode);
return lang != QLocale::AnyLanguage ? QVariant::fromValue(lang) : QVariant(langStr);
}
QMediaPlayer::Error mapMediaEngineError(MF_MEDIA_ENGINE_ERR err)
{
switch (err) {
case MF_MEDIA_ENGINE_ERR_NOERROR:
return QMediaPlayer::NoError;
case MF_MEDIA_ENGINE_ERR_ABORTED:
return QMediaPlayer::ResourceError;
case MF_MEDIA_ENGINE_ERR_NETWORK:
return QMediaPlayer::NetworkError;
case MF_MEDIA_ENGINE_ERR_DECODE:
return QMediaPlayer::FormatError;
case MF_MEDIA_ENGINE_ERR_SRC_NOT_SUPPORTED:
return QMediaPlayer::ResourceError;
case MF_MEDIA_ENGINE_ERR_ENCRYPTED:
return QMediaPlayer::AccessDeniedError;
default:
return QMediaPlayer::ResourceError;
}
}
std::chrono::microseconds videoFrameDurationUs(IMFMediaEngineEx *engine)
{
if (!engine)
return {};
DWORD streamCount = 0;
if (FAILED(engine->GetNumberOfStreams(&streamCount)))
return {};
for (DWORD i = 0; i < streamCount; ++i) {
ScopedPropVariant majorType;
if (FAILED(engine->GetStreamAttribute(i, MF_MT_MAJOR_TYPE, majorType.get()))
|| majorType->vt != VT_CLSID || *majorType->puuid != MFMediaType_Video) {
continue;
}
ScopedPropVariant rate;
if (FAILED(engine->GetStreamAttribute(i, MF_MT_FRAME_RATE, rate.get()))
|| rate->vt != VT_UI8) {
return {};
}
UINT64 avgTimePerFrame = 0;
if (SUCCEEDED(MFFrameRateToAverageTimePerFrame(
HI32(rate->uhVal.QuadPart), LO32(rate->uhVal.QuadPart), &avgTimePerFrame)))
return std::chrono::round<std::chrono::microseconds>(reference_time(avgTimePerFrame));
return {};
}
return {};
}
} // namespace
bool QMediaEnginePlayer::initD3D11()
{
m_textureImporter = std::make_shared<QtMultimediaPrivate::QWindowsD3D11RhiTextureImporter>();
const D3D_FEATURE_LEVEL featureLevels[] = {
D3D_FEATURE_LEVEL_11_1,
D3D_FEATURE_LEVEL_11_0,
};
UINT flags = D3D11_CREATE_DEVICE_VIDEO_SUPPORT | D3D11_CREATE_DEVICE_BGRA_SUPPORT;
HRESULT hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, flags, featureLevels,
ARRAYSIZE(featureLevels), D3D11_SDK_VERSION, &m_d3dDevice,
nullptr, nullptr);
if (FAILED(hr)) {
qCWarning(lcMediaEngine) << "D3D11CreateDevice:" << QSystemError::windowsComString(hr);
return false;
}
ComPtr<ID3D10Multithread> multithread;
if (SUCCEEDED(m_d3dDevice.As(&multithread)))
multithread->SetMultithreadProtected(TRUE);
return true;
}
bool QMediaEnginePlayer::initMediaEngine()
{
HRESULT hr = MFCreateDXGIDeviceManager(&m_resetToken, &m_dxgiManager);
if (FAILED(hr)) {
qCWarning(lcMediaEngine) << "MFCreateDXGIDeviceManager:"
<< QSystemError::windowsComString(hr);
return false;
}
hr = m_dxgiManager->ResetDevice(m_d3dDevice.Get(), m_resetToken);
if (FAILED(hr)) {
qCWarning(lcMediaEngine) << "ResetDevice:" << QSystemError::windowsComString(hr);
return false;
}
m_notify = makeComObject<QMediaEngineNotify>();
// Queued: some MF events fire synchronously (e.g. PAUSE from Pause()),
// must defer to avoid re-entering handleMediaEngineEvent mid-operation.
QObject::connect(m_notify.Get(), &QMediaEngineNotify::eventNotified, context(),
[this](DWORD event, DWORD_PTR param1, DWORD param2) {
handleMediaEngineEvent(event, param1, param2);
}, Qt::QueuedConnection);
ComPtr<IMFAttributes> attrs;
hr = MFCreateAttributes(&attrs, 4);
if (FAILED(hr)) {
qCWarning(lcMediaEngine) << "MFCreateAttributes:" << QSystemError::windowsComString(hr);
return false;
}
attrs->SetUnknown(MF_MEDIA_ENGINE_CALLBACK, m_notify.Get());
attrs->SetUnknown(MF_MEDIA_ENGINE_DXGI_MANAGER, m_dxgiManager.Get());
attrs->SetUINT32(MF_MEDIA_ENGINE_VIDEO_OUTPUT_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM);
ComPtr<IMFMediaEngineClassFactory> factory;
hr = CoCreateInstance(CLSID_MFMediaEngineClassFactory, nullptr, CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&factory));
if (FAILED(hr)) {
qCWarning(lcMediaEngine) << "CoCreateInstance(MFMediaEngineClassFactory):"
<< QSystemError::windowsComString(hr);
return false;
}
hr = factory->CreateInstance(0, attrs.Get(), &m_mediaEngine);
if (FAILED(hr)) {
qCWarning(lcMediaEngine) << "IMFMediaEngine::CreateInstance:"
<< QSystemError::windowsComString(hr);
return false;
}
hr = m_mediaEngine.As(&m_mediaEngineEx);
if (FAILED(hr)) {
qCWarning(lcMediaEngine) << "IMFMediaEngineEx:" << QSystemError::windowsComString(hr);
return false;
}
// Enable inband timed text (embedded subtitles) discovery.
// IMFTimedText is obtained via MFGetService with MF_MEDIA_ENGINE_TIMEDTEXT.
ComPtr<IMFGetService> getService;
if (SUCCEEDED(m_mediaEngine.As(&getService))) {
getService->GetService(MF_MEDIA_ENGINE_TIMEDTEXT, IID_PPV_ARGS(&m_timedText));
}
if (m_timedText) {
m_timedText->SetInBandEnabled(TRUE);
m_timedTextNotify = makeComObject<QMediaEngineTimedTextNotify>();
QObject::connect(m_timedTextNotify.Get(), &QMediaEngineTimedTextNotify::trackAdded,
context(), [this] {
updateSubtitleTracks();
});
QObject::connect(m_timedTextNotify.Get(), &QMediaEngineTimedTextNotify::subtitleTextChanged,
context(), [this](const QString &text) {
if (m_videoSink)
m_videoSink->setSubtitleText(text);
});
m_timedText->RegisterNotifications(m_timedTextNotify.Get());
}
// Apply cached playback rate to the newly created engine.
if (!qFuzzyCompare(m_playbackRate, 1.0)) {
m_mediaEngine->SetDefaultPlaybackRate(m_playbackRate);
m_mediaEngine->SetPlaybackRate(m_playbackRate);
}
return true;
}
void QMediaEnginePlayer::releaseMediaEngine()
{
if (m_mediaEngine) {
m_mediaEngine->Shutdown();
m_mediaEngine.Reset();
}
m_mediaEngineEx.Reset();
m_timedText.Reset();
m_timedTextNotify.Reset();
m_notify.Reset();
m_dxgiManager.Reset();
m_resetToken = 0;
}
void QMediaEnginePlayer::startPlaybackTimers()
{
m_positionUpdateTimer.start();
m_videoRenderTimer.start();
}
void QMediaEnginePlayer::stopPlaybackTimers()
{
m_positionUpdateTimer.stop();
m_videoRenderTimer.stop();
}
QMediaEnginePlayer::QMediaEnginePlayer(QMediaPlayer *player) : QPlatformMediaPlayer(player)
{
if (!initD3D11())
error(QMediaPlayer::ResourceError, u"Failed to initialize D3D11"_s);
m_positionUpdateTimer.callOnTimeout(context(), [this] {
if (m_mediaEngine && !m_seekTarget)
positionChanged(currentPositionMs());
});
m_videoRenderTimer.callOnTimeout(context(), [this] {
presentVideoFrame();
});
m_seekTimeoutTimer.setSingleShot(true);
m_seekTimeoutTimer.callOnTimeout(context(), [this] {
if (m_seekTarget) {
qCWarning(lcMediaEngine) << "Seek timeout — SEEKED event not received, resetting";
m_seekTarget.reset();
}
});
}
QMediaEnginePlayer::~QMediaEnginePlayer()
{
stopPlaybackTimers();
m_seekTimeoutTimer.stop();
m_metadataFuture.cancel();
releaseMediaEngine();
}
void QMediaEnginePlayer::handleMediaEngineEvent(DWORD event, DWORD_PTR param1, DWORD param2)
{
Q_UNUSED(param2);
if (!m_mediaEngine)
return;
switch (event) {
case MF_MEDIA_ENGINE_EVENT_LOADEDMETADATA:
qCDebug(lcMediaEngine) << "LOADEDMETADATA";
updateMetaData();
updateTracks();
updateDuration();
break;
case MF_MEDIA_ENGINE_EVENT_LOADEDDATA: {
qCDebug(lcMediaEngine) << "LOADEDDATA";
audioAvailableChanged(m_mediaEngine->HasAudio());
videoAvailableChanged(m_mediaEngine->HasVideo());
updateVideoFrameFormat();
updateNativeSize();
seekableChanged(!m_stream || !m_stream->isSequential());
mediaStatusChanged(QMediaPlayer::LoadedMedia);
if (const float bp = bufferProgress(); bp > 0)
bufferProgressChanged(bp);
if (m_pendingPlay) {
m_pendingPlay = false;
play();
}
break;
}
case MF_MEDIA_ENGINE_EVENT_CANPLAY:
qCDebug(lcMediaEngine) << "CANPLAY";
if (state() == QMediaPlayer::PlayingState || mediaStatus() == QMediaPlayer::BufferingMedia
|| m_pendingPlay) {
mediaStatusChanged(QMediaPlayer::BufferedMedia);
}
bufferProgressChanged(1.0f);
break;
case MF_MEDIA_ENGINE_EVENT_PLAYING:
qCDebug(lcMediaEngine) << "PLAYING";
if (state() != QMediaPlayer::PlayingState)
stateChanged(QMediaPlayer::PlayingState);
if (mediaStatus() == QMediaPlayer::BufferingMedia)
mediaStatusChanged(QMediaPlayer::BufferedMedia);
startPlaybackTimers();
break;
case MF_MEDIA_ENGINE_EVENT_PAUSE:
qCDebug(lcMediaEngine) << "PAUSE";
// Events are queued: ignore a stale PAUSE if playback was resumed meanwhile.
if (!m_mediaEngine->IsPaused())
break;
if (state() != QMediaPlayer::StoppedState && state() != QMediaPlayer::PausedState) {
stateChanged(QMediaPlayer::PausedState);
stopPlaybackTimers();
}
break;
case MF_MEDIA_ENGINE_EVENT_ENDED: {
qCDebug(lcMediaEngine) << "ENDED";
stopPlaybackTimers();
if (mediaStatus() == QMediaPlayer::BufferingMedia)
mediaStatusChanged(QMediaPlayer::BufferedMedia);
if (doLoop()) {
m_mediaEngine->SetCurrentTime(0);
m_mediaEngine->Play();
} else {
if (m_videoSink)
m_videoSink->setVideoFrame({});
positionChanged(m_duration.count());
stateChanged(QMediaPlayer::StoppedState);
mediaStatusChanged(QMediaPlayer::EndOfMedia);
bufferProgressChanged(0.f);
resetCurrentLoop();
}
break;
}
case MF_MEDIA_ENGINE_EVENT_TIMEUPDATE:
if (!m_seekTarget)
positionChanged(currentPositionMs());
break;
case MF_MEDIA_ENGINE_EVENT_ERROR: {
const auto mfErr = static_cast<MF_MEDIA_ENGINE_ERR>(param1);
QString detail;
switch (mfErr) {
case MF_MEDIA_ENGINE_ERR_ABORTED:
detail = u"Playback aborted"_s;
break;
case MF_MEDIA_ENGINE_ERR_NETWORK:
detail = u"Network error"_s;
break;
case MF_MEDIA_ENGINE_ERR_DECODE:
detail = u"Unsupported or corrupt media"_s;
break;
case MF_MEDIA_ENGINE_ERR_SRC_NOT_SUPPORTED:
detail = u"Source not supported"_s;
break;
case MF_MEDIA_ENGINE_ERR_ENCRYPTED:
detail = u"Encrypted content"_s;
break;
default:
detail = u"Unknown error %1"_s.arg(param1);
break;
}
qCWarning(lcMediaEngine) << "Error:" << detail;
error(mapMediaEngineError(mfErr), detail);
mediaStatusChanged(QMediaPlayer::InvalidMedia);
stopPlaybackTimers();
break;
}
case MF_MEDIA_ENGINE_EVENT_BUFFERINGSTARTED:
qCDebug(lcMediaEngine) << "BUFFERINGSTARTED";
mediaStatusChanged(QMediaPlayer::BufferingMedia);
bufferProgressChanged(bufferProgress());
break;
case MF_MEDIA_ENGINE_EVENT_BUFFERINGENDED:
qCDebug(lcMediaEngine) << "BUFFERINGENDED";
mediaStatusChanged(QMediaPlayer::BufferedMedia);
bufferProgressChanged(bufferProgress());
break;
case MF_MEDIA_ENGINE_EVENT_DURATIONCHANGE:
qCDebug(lcMediaEngine) << "DURATIONCHANGE";
updateDuration();
break;
case MF_MEDIA_ENGINE_EVENT_FORMATCHANGE:
qCDebug(lcMediaEngine) << "FORMATCHANGE";
updateVideoFrameFormat();
break;
case MF_MEDIA_ENGINE_EVENT_FIRSTFRAMEREADY:
qCDebug(lcMediaEngine) << "FIRSTFRAMEREADY";
if (state() != QMediaPlayer::StoppedState)
presentVideoFrame();
break;
case MF_MEDIA_ENGINE_EVENT_SEEKED:
qCDebug(lcMediaEngine) << "SEEKED";
m_seekTimeoutTimer.stop();
m_seekTarget.reset();
break;
default:
break;
}
}
void QMediaEnginePlayer::setMedia(const QUrl &media, QIODevice *stream)
{
qCDebug(lcMediaEngine) << "setMedia:" << media << "stream:" << stream;
if (state() != QMediaPlayer::StoppedState)
stop();
m_url = media;
m_stream = stream;
m_duration = {};
m_pendingPlay = false;
m_seekTarget.reset();
m_seekTimeoutTimer.stop();
seekableChanged(false);
m_videoSize = {};
m_transferTexture.Reset();
m_metaData = {};
m_metadataFuture.cancel();
if (m_videoSink)
m_videoSink->setVideoFrame({});
m_tracks = {};
if (media.isEmpty() && !stream) {
audioAvailableChanged(false);
videoAvailableChanged(false);
// IMFMediaEngine has no API to clear the current source without
// shutting down. Release the engine now; it will be lazily
// recreated on the next setMedia() call with a real source.
releaseMediaEngine();
metaDataChanged();
mediaStatusChanged(QMediaPlayer::NoMedia);
return;
}
// Lazy init: create the MediaEngine on first use,
// or recreate it after a previous NoMedia release.
if (!m_mediaEngine) {
if (!initMediaEngine()) {
error(QMediaPlayer::ResourceError, u"MediaEngine not initialized"_s);
mediaStatusChanged(QMediaPlayer::InvalidMedia);
return;
}
}
HRESULT hr = E_FAIL;
if (stream) {
ComPtr<MFStream> mfStream = makeComObject<MFStream>(stream, false);
QString urlStr = media.toString();
// SetSourceFromByteStream requires a non-null URL.
// Use the real file name when available so that Media Foundation
// can infer the MIME type from the extension.
if (urlStr.isEmpty()) {
if (auto *file = qobject_cast<QFile *>(stream))
urlStr = u"file:///"_s + file->fileName();
else
urlStr = u"stream:///media"_s;
}
const QBStr bstrUrl(urlStr);
hr = m_mediaEngineEx->SetSourceFromByteStream(mfStream.Get(), bstrUrl.bstr());
} else {
const QBStr bstrUrl(media.toString());
hr = m_mediaEngine->SetSource(bstrUrl.bstr());
}
if (FAILED(hr)) {
qCWarning(lcMediaEngine) << "SetSource:" << QSystemError::windowsComString(hr);
error(QMediaPlayer::ResourceError, u"Failed to set media source"_s);
mediaStatusChanged(QMediaPlayer::InvalidMedia);
return;
}
mediaStatusChanged(QMediaPlayer::LoadingMedia);
applyAudioOutput();
}
void QMediaEnginePlayer::play()
{
if (!m_mediaEngine)
return;
if (mediaStatus() == QMediaPlayer::NoMedia || mediaStatus() == QMediaPlayer::InvalidMedia) {
return;
}
if (mediaStatus() == QMediaPlayer::LoadingMedia) {
m_pendingPlay = true;
return;
}
if (mediaStatus() == QMediaPlayer::EndOfMedia) {
if (!m_seekTarget) {
// Play() on an ended engine restarts from the beginning (HTML5 semantics).
// Seeking explicitly before Play() makes the engine report ENDED again.
if (!m_mediaEngine->IsEnded())
m_mediaEngine->SetCurrentTime(0);
positionChanged(0);
}
resetCurrentLoop();
mediaStatusChanged(QMediaPlayer::BufferingMedia);
} else if (mediaStatus() == QMediaPlayer::LoadedMedia
|| mediaStatus() == QMediaPlayer::BufferedMedia) {
mediaStatusChanged(QMediaPlayer::BufferingMedia);
}
HRESULT hr = m_mediaEngine->Play();
if (FAILED(hr)) {
qCWarning(lcMediaEngine) << "Play:" << QSystemError::windowsComString(hr);
return;
}
stateChanged(QMediaPlayer::PlayingState);
startPlaybackTimers();
}
void QMediaEnginePlayer::pause()
{
if (!m_mediaEngine)
return;
m_pendingPlay = false;
// Like play(), pause() from EndOfMedia rewinds to the beginning.
const bool fromEndOfMedia = mediaStatus() == QMediaPlayer::EndOfMedia;
if (fromEndOfMedia) {
if (!m_seekTarget)
setPosition(0);
resetCurrentLoop();
}
HRESULT hr = m_mediaEngine->Pause();
if (FAILED(hr)) {
qCWarning(lcMediaEngine) << "Pause:" << QSystemError::windowsComString(hr);
return;
}
if (fromEndOfMedia)
mediaStatusChanged(QMediaPlayer::BufferedMedia);
if (mediaStatus() != QMediaPlayer::NoMedia && mediaStatus() != QMediaPlayer::InvalidMedia) {
stateChanged(QMediaPlayer::PausedState);
m_positionUpdateTimer.stop();
}
// Briefly start the render timer to deliver the current frame.
if (isVideoAvailable() && !m_videoRenderTimer.isActive())
m_videoRenderTimer.start();
}
void QMediaEnginePlayer::stop()
{
if (!m_mediaEngine)
return;
m_pendingPlay = false;
m_seekTarget.reset();
m_seekTimeoutTimer.stop();
stopPlaybackTimers();
// Don't call Pause() if already at EndOfMedia — the engine is already
// stopped internally, and Pause() in this state can prevent subsequent
// Play() from working.
if (mediaStatus() != QMediaPlayer::EndOfMedia) {
HRESULT hr = m_mediaEngine->Pause();
if (FAILED(hr))
qCWarning(lcMediaEngine) << "Pause (in stop):" << QSystemError::windowsComString(hr);
}
// Flush the current position before SetCurrentTime(0) so that
// QPlatformMediaPlayer's duplicate filter doesn't suppress positionChanged(0).
positionChanged(currentPositionMs());
m_mediaEngine->SetCurrentTime(0);
if (m_videoSink)
m_videoSink->setVideoFrame({});
stateChanged(QMediaPlayer::StoppedState);
positionChanged(0);
if (mediaStatus() == QMediaPlayer::BufferedMedia
|| mediaStatus() == QMediaPlayer::BufferingMedia
|| mediaStatus() == QMediaPlayer::EndOfMedia) {
mediaStatusChanged(QMediaPlayer::LoadedMedia);
bufferProgressChanged(0);
}
resetCurrentLoop();
}
qint64 QMediaEnginePlayer::position() const
{
if (!m_mediaEngine)
return 0;
if (m_seekTarget.has_value())
return m_seekTarget->count();
return currentPositionMs();
}
qint64 QMediaEnginePlayer::currentPositionMs() const
{
if (!m_mediaEngine)
return 0;
const qint64 pos = qRound64(m_mediaEngine->GetCurrentTime() * 1000.0);
// GetCurrentTime() can overshoot the reported duration slightly.
return m_duration.count() > 0 ? qMin(pos, m_duration.count()) : pos;
}
QSize QMediaEnginePlayer::nativeVideoSize() const
{
if (!m_mediaEngine)
return {};
DWORD w = 0, h = 0;
m_mediaEngine->GetNativeVideoSize(&w, &h);
return { static_cast<int>(w), static_cast<int>(h) };
}
void QMediaEnginePlayer::setPosition(qint64 position)
{
if (!m_mediaEngine)
return;
position = qMax(qint64(0), position);
if (const qint64 dur = duration(); dur > 0)
position = qMin(position, dur);
// IMFMediaEngine auto-resumes after a seek from the ended state
// (per HTML5 spec). Remember the ended state before SetCurrentTime
// resets it, so we can pause immediately after to prevent silent
// playback when our state is Stopped or Paused.
const bool wasEnded = m_mediaEngine->IsEnded();
m_seekTarget = std::chrono::milliseconds(position);
m_mediaEngine->SetCurrentTime(position / 1000.0);
if (wasEnded && state() != QMediaPlayer::PlayingState)
m_mediaEngine->Pause();
// SEEKED may not arrive after device lost or rapid seeks.
// Reset m_seekTarget on timeout to unblock position/frame updates.
m_seekTimeoutTimer.start();
if (mediaStatus() == QMediaPlayer::EndOfMedia)
mediaStatusChanged(QMediaPlayer::LoadedMedia);
positionChanged(position);
if (isVideoAvailable() && state() == QMediaPlayer::PausedState)
m_videoRenderTimer.start();
}
qreal QMediaEnginePlayer::playbackRate() const
{
return m_playbackRate;
}
void QMediaEnginePlayer::setPlaybackRate(qreal rate)
{
if (qFuzzyCompare(rate, m_playbackRate))
return;
m_playbackRate = rate;
if (m_mediaEngine) {
m_mediaEngine->SetDefaultPlaybackRate(rate);
m_mediaEngine->SetPlaybackRate(rate);
}
playbackRateChanged(rate);
}
float QMediaEnginePlayer::bufferProgress() const
{
if (!m_mediaEngine)
return 0;
ComPtr<IMFMediaTimeRange> buffered;
if (FAILED(m_mediaEngine->GetBuffered(&buffered)) || !buffered)
return 0;
const DWORD count = buffered->GetLength();
if (count == 0)
return 0;
const double dur = m_mediaEngine->GetDuration();
if (!std::isfinite(dur) || dur <= 0)
return 0;
double end = 0;
buffered->GetEnd(count - 1, &end);
return static_cast<float>(qBound(0.0, end / dur, 1.0));
}
QMediaTimeRange QMediaEnginePlayer::availablePlaybackRanges() const
{
QMediaTimeRange ranges;
if (!m_mediaEngine)
return ranges;
ComPtr<IMFMediaTimeRange> buffered;
if (FAILED(m_mediaEngine->GetBuffered(&buffered)) || !buffered)
return ranges;
const DWORD count = buffered->GetLength();
for (DWORD i = 0; i < count; ++i) {
double start = 0, end = 0;
buffered->GetStart(i, &start);
buffered->GetEnd(i, &end);
ranges.addInterval(qRound64(start * 1000), qRound64(end * 1000));
}
return ranges;
}
qint64 QMediaEnginePlayer::duration() const
{
return m_duration.count();
}
QUrl QMediaEnginePlayer::media() const
{
return m_url;
}
const QIODevice *QMediaEnginePlayer::mediaStream() const
{
return m_stream;
}
QMediaEnginePlayer::PitchCompensationAvailability
QMediaEnginePlayer::pitchCompensationAvailability() const
{
return PitchCompensationAvailability::AlwaysOn;
}
QMediaMetaData QMediaEnginePlayer::metaData() const
{
return m_metaData;
}
void QMediaEnginePlayer::presentVideoFrame()
{
if (!m_mediaEngine || !isVideoAvailable())
return;
// Auto-stop the render timer when not in playing state, regardless of
// which code path we take (including early returns). Keep it running
// while a seek is pending so we can deliver the post-seek frame.
const bool isPlaying = (state() == QMediaPlayer::PlayingState);
auto stopGuard = qScopeGuard([&] {
if (!isPlaying && !m_seekTarget)
m_videoRenderTimer.stop();
});
LONGLONG pts = 0;
if (m_mediaEngine->OnVideoStreamTick(&pts) != S_OK)
return;
// While a seek is pending, skip frame delivery — the engine may
// still return a frame from the pre-seek position.
if (m_seekTarget)
return;
if (!m_videoSink)
return;
const QSize videoSize = nativeVideoSize();
if (videoSize.isEmpty())
return;
if (videoSize != m_videoSize) {
m_videoSize = videoSize;
m_transferTexture.Reset();
}
if (!m_transferTexture) {
D3D11_TEXTURE2D_DESC desc = {};
desc.Width = videoSize.width();
desc.Height = videoSize.height();
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
desc.SampleDesc.Count = 1;
desc.Usage = D3D11_USAGE_DEFAULT;
desc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
desc.MiscFlags = 0;
HRESULT hr = m_d3dDevice->CreateTexture2D(&desc, nullptr, &m_transferTexture);
if (FAILED(hr)) {
qCWarning(lcMediaEngine) << "CreateTexture2D:" << QSystemError::windowsComString(hr);
return;
}
updateVideoFrameFormat();
}
MFVideoNormalizedRect srcRect = { 0.0f, 0.0f, 1.0f, 1.0f };
RECT destRect = { 0, 0, videoSize.width(), videoSize.height() };
HRESULT hr = m_mediaEngine->TransferVideoFrame(m_transferTexture.Get(), &srcRect, &destRect,
nullptr);
if (FAILED(hr)) {
if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET) {
if (m_deviceLostRetries >= kMaxDeviceLostRetries) {
qCWarning(lcMediaEngine) << "Device lost recovery failed after"
<< m_deviceLostRetries << "attempts, giving up";
error(QMediaPlayer::ResourceError, u"D3D11 device lost recovery failed"_s);
return;
}
++m_deviceLostRetries;
qCWarning(lcMediaEngine) << "Device lost during TransferVideoFrame, recovering"
<< "(attempt" << m_deviceLostRetries << ")...";
const qint64 savedPosition = currentPositionMs();
const auto savedState = state();
stopPlaybackTimers();
// Release dead resources immediately — the device is already lost.
releaseMediaEngine();
m_transferTexture.Reset();
m_d3dDevice.Reset();
// Defer re-creation to avoid re-entering presentVideoFrame().
QTimer::singleShot(0, context(), [this, savedPosition, savedState] {
if (!initD3D11()) {
error(QMediaPlayer::ResourceError, u"D3D11 device recreation failed"_s);
return;
}
if (!m_url.isEmpty() || m_stream) {
setMedia(m_url, m_stream);
if (savedPosition > 0)
setPosition(savedPosition);
if (savedState == QMediaPlayer::PlayingState)
play();
}
});
return;
}
qCWarning(lcMediaEngine) << "TransferVideoFrame:" << QSystemError::windowsComString(hr);
return;
}
// Successful frame transfer — reset device lost counter.
m_deviceLostRetries = 0;
auto buffer = std::make_unique<QMediaEngineVideoBuffer>(m_transferTexture, m_d3dDevice,
videoSize, m_textureImporter);
QVideoFrame frame = QVideoFramePrivate::createFrame(std::move(buffer), m_videoFormat);
auto startTime = std::chrono::round<std::chrono::microseconds>(reference_time(pts));
frame.setStartTime(startTime.count());
if (m_frameDuration > 0us)
frame.setEndTime((startTime + m_frameDuration).count());
m_videoSink->setVideoFrame(frame);
m_videoSink->setNativeSize(videoSize);
}
// IMFMediaEngine already applies rotation to the decoded pixels, so
// the video format does not include rotation — VideoOutput would apply
// it a second time.
void QMediaEnginePlayer::updateVideoFrameFormat()
{
const QSize size = nativeVideoSize();
if (size.isEmpty())
return;
m_videoFormat = QVideoFrameFormat(size, QVideoFrameFormat::Format_BGRA8888);
m_frameDuration = videoFrameDurationUs(m_mediaEngineEx.Get());
}
void QMediaEnginePlayer::updateNativeSize()
{
if (!m_videoSink)
return;
const QSize size = nativeVideoSize();
if (!size.isEmpty())
m_videoSink->setNativeSize(size);
}
void QMediaEnginePlayer::setVideoSink(QVideoSink *sink)
{
auto *newSink = sink ? sink->platformVideoSink() : nullptr;
auto *oldSink = m_videoSink;
m_videoSink = newSink;
updateNativeSize();
// Transfer the current frame to the new sink (e.g. when switching
// sinks while paused — OnVideoStreamTick won't return the same frame).
if (newSink && oldSink)
newSink->setVideoFrame(oldSink->currentVideoFrame());
}
void QMediaEnginePlayer::setAudioOutput(QPlatformAudioOutput *output)
{
if (m_audioOutput == output)
return;
if (m_audioOutput && m_audioOutput->q)
m_audioOutput->q->disconnect(context());
m_audioOutput = output;
if (output && output->q) {
QObject::connect(output->q, &QAudioOutput::volumeChanged, context(), [this] {
applyAudioOutput();
});
QObject::connect(output->q, &QAudioOutput::mutedChanged, context(), [this] {
applyAudioOutput();
});
QObject::connect(output->q, &QAudioOutput::deviceChanged, context(), [this] {
applyAudioOutput();
});
}
applyAudioOutput();
}
void QMediaEnginePlayer::applyAudioOutput()
{
if (!m_mediaEngine)
return;
if (!m_audioOutput) {
m_mediaEngine->SetMuted(TRUE);
return;
}
m_mediaEngine->SetVolume(static_cast<double>(m_audioOutput->volume));
m_mediaEngine->SetMuted(m_audioOutput->muted ? TRUE : FALSE);
const QByteArray id = m_audioOutput->device.id();
if (!id.isEmpty()) {
ComPtr<IMFMediaEngineAudioEndpointId> endpointId;
if (SUCCEEDED(m_mediaEngine.As(&endpointId))) {
const QString deviceId = QString::fromUtf8(id);
endpointId->SetAudioEndpointId(reinterpret_cast<LPCWSTR>(deviceId.utf16()));
}
}
}
void QMediaEnginePlayer::updateMetaData()
{
if (!m_mediaEngineEx)
return;
m_metaData = {};
const QSize size = nativeVideoSize();
if (!size.isEmpty())
m_metaData.insert(QMediaMetaData::Resolution, size);
if (m_duration > 0ms)
m_metaData.insert(QMediaMetaData::Duration, m_duration.count());
m_metadataFuture.cancel();
const QUrl url = m_url;
// IMFSourceResolver needs a URL; skip for pure QIODevice streams.
if (url.isEmpty()) {
metaDataChanged();
return;
}
m_metadataFuture = QtConcurrent::run([url]() -> QMediaMetaData {
ComPtr<IMFSourceResolver> resolver;
if (FAILED(MFCreateSourceResolver(&resolver)))
return {};
ComPtr<IUnknown> sourceUnk;
MF_OBJECT_TYPE objectType = MF_OBJECT_INVALID;
HRESULT hr = E_FAIL;
if (url.scheme() == u"qrc"_s) {
auto file = std::make_unique<QFile>(u':' + url.path());
if (!file->open(QIODevice::ReadOnly))
return {};
// MFStream in synchronous mode: BeginRead performs the read
// immediately and invokes the callback on the calling thread,
// so no event loop is needed. ownStream=true so MFStream
// takes ownership of the QFile.
ComPtr<MFStream> mfStream =
makeComObject<MFStream>(file.release(), true, true /*synchronous*/);
const auto urlStr = url.toString();
hr = resolver->CreateObjectFromByteStream(
mfStream.Get(), reinterpret_cast<LPCWSTR>(urlStr.utf16()),
MF_RESOLUTION_MEDIASOURCE
| MF_RESOLUTION_CONTENT_DOES_NOT_HAVE_TO_MATCH_EXTENSION_OR_MIME_TYPE,
nullptr, &objectType, &sourceUnk);
} else {
const auto urlStr = url.toString();
hr = resolver->CreateObjectFromURL(
reinterpret_cast<LPCWSTR>(urlStr.utf16()),
MF_RESOLUTION_MEDIASOURCE
| MF_RESOLUTION_CONTENT_DOES_NOT_HAVE_TO_MATCH_EXTENSION_OR_MIME_TYPE,
nullptr, &objectType, &sourceUnk);
}
if (FAILED(hr) || !sourceUnk)
return {};
ComPtr<IMFMediaSource> mediaSource;
if (FAILED(sourceUnk.As(&mediaSource)))
return {};
QMediaMetaData result = MFMetaData::fromNative(mediaSource.Get());
mediaSource->Shutdown();
return result;
}).then(context(), [this](QMediaMetaData rich) {
if (!rich.isEmpty()) {
for (const auto &[key, value] : std::as_const(m_metaData).asKeyValueRange())
rich.insert(key, value);
m_metaData = rich;
}
metaDataChanged();
});
}
void QMediaEnginePlayer::updateTracks()
{
if (!m_mediaEngineEx)
return;
m_tracks = {};
DWORD streamCount = 0;
if (FAILED(m_mediaEngineEx->GetNumberOfStreams(&streamCount)))
return;
for (DWORD i = 0; i < streamCount; ++i) {
std::optional<TrackType> trackType;
{
ScopedPropVariant var;
if (FAILED(m_mediaEngineEx->GetStreamAttribute(i, MF_MT_MAJOR_TYPE, var.get())))
continue;
if (var->vt == VT_CLSID) {
if (*var->puuid == MFMediaType_Audio)
trackType = TrackType::AudioStream;
else if (*var->puuid == MFMediaType_Video)
trackType = TrackType::VideoStream;
}
}
if (!trackType)
continue;
QMediaMetaData trackMeta;
{
ScopedPropVariant var;
if (SUCCEEDED(m_mediaEngineEx->GetStreamAttribute(i, MF_SD_LANGUAGE, var.get()))) {
if (var->vt == VT_LPWSTR && var->pwszVal)
trackMeta.insert(QMediaMetaData::Language,
languageVariant(QString::fromWCharArray(var->pwszVal)));
}
}
m_tracks[*trackType].entries.push_back({ trackMeta, i });
BOOL selected = FALSE;
if (SUCCEEDED(m_mediaEngineEx->GetStreamSelection(i, &selected)) && selected)
m_tracks[*trackType].currentIndex =
static_cast<int>(m_tracks[*trackType].entries.size()) - 1;
}
updateSubtitleTracks();
tracksChanged();
}
void QMediaEnginePlayer::updateSubtitleTracks()
{
auto &subtitleInfo = m_tracks[TrackType::SubtitleStream];
subtitleInfo = {};
if (!m_timedText)
return;
ComPtr<IMFTimedTextTrackList> trackList;
if (FAILED(m_timedText->GetTextTracks(&trackList)) || !trackList)
return;
const DWORD count = trackList->GetLength();
for (DWORD i = 0; i < count; ++i) {
ComPtr<IMFTimedTextTrack> track;
if (FAILED(trackList->GetTrack(i, &track)) || !track)
continue;
QMediaMetaData trackMeta;
QComTaskResource<WCHAR> label;
if (SUCCEEDED(track->GetLabel(label.address())) && label)
trackMeta.insert(QMediaMetaData::Title, QString::fromWCharArray(label.get()));
QComTaskResource<WCHAR> lang;
if (SUCCEEDED(track->GetLanguage(lang.address())) && lang)
trackMeta.insert(QMediaMetaData::Language,
languageVariant(QString::fromWCharArray(lang.get())));
// Store the timed-text track ID as the streamIndex for SelectTrack.
const DWORD trackId = track->GetId();
subtitleInfo.entries.push_back({ trackMeta, trackId });
if (track->IsActive()) {
subtitleInfo.currentIndex = static_cast<int>(subtitleInfo.entries.size()) - 1;
}
}
}
void QMediaEnginePlayer::updateDuration()
{
if (!m_mediaEngine)
return;
const double dur = m_mediaEngine->GetDuration();
const auto newDuration = std::chrono::milliseconds(
(std::isfinite(dur) && dur >= 0) ? qRound64(dur * 1000.0) : 0);
if (newDuration != m_duration) {
m_duration = newDuration;
durationChanged(m_duration.count());
}
}
int QMediaEnginePlayer::trackCount(TrackType type)
{
return static_cast<int>(m_tracks[type].entries.size());
}
QMediaMetaData QMediaEnginePlayer::trackMetaData(TrackType type, int trackNumber)
{
if (trackNumber < 0 || trackNumber >= static_cast<int>(m_tracks[type].entries.size()))
return {};
return m_tracks[type].entries.at(trackNumber).metaData;
}
int QMediaEnginePlayer::activeTrack(TrackType type)
{
return m_tracks[type].currentIndex;
}
void QMediaEnginePlayer::setActiveTrack(TrackType type, int index)
{
if (index < -1 || index >= static_cast<int>(m_tracks[type].entries.size()))
return;
if (type == TrackType::SubtitleStream) {
// Subtitle tracks are managed via IMFTimedText, not stream selection.
if (!m_timedText)
return;
const int currentIdx = m_tracks[type].currentIndex;
if (currentIdx >= 0 && currentIdx < static_cast<int>(m_tracks[type].entries.size())) {
DWORD trackId = m_tracks[type].entries.at(currentIdx).streamIndex;
m_timedText->SelectTrack(trackId, FALSE);
}
if (index >= 0) {
DWORD trackId = m_tracks[type].entries.at(index).streamIndex;
m_timedText->SelectTrack(trackId, TRUE);
}
if (m_videoSink)
m_videoSink->setSubtitleText({});
m_tracks[type].currentIndex = index;
activeTracksChanged();
return;
}
if (!m_mediaEngineEx)
return;
const int currentIdx = m_tracks[type].currentIndex;
if (currentIdx >= 0 && currentIdx < static_cast<int>(m_tracks[type].entries.size())) {
DWORD streamIdx = m_tracks[type].entries.at(currentIdx).streamIndex;
m_mediaEngineEx->SetStreamSelection(streamIdx, FALSE);
}
if (index >= 0) {
DWORD streamIdx = m_tracks[type].entries.at(index).streamIndex;
m_mediaEngineEx->SetStreamSelection(streamIdx, TRUE);
}
m_tracks[type].currentIndex = index;
activeTracksChanged();
}
QT_END_NAMESPACE