| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:LGPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU Lesser General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU Lesser |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
| ** packaging of this file. Please review the following information to |
| ** ensure the GNU Lesser General Public License version 3 requirements |
| ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 2.0 or (at your option) the GNU General |
| ** Public license version 3 or any later version approved by the KDE Free |
| ** Qt Foundation. The licenses are as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
| ** https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include "evrcustompresenter.h" |
| |
| #include "evrd3dpresentengine.h" |
| #include "evrhelpers.h" |
| |
| #include <QtCore/qmutex.h> |
| #include <QtCore/qvarlengtharray.h> |
| #include <QtCore/qrect.h> |
| #include <qabstractvideosurface.h> |
| #include <qthread.h> |
| #include <qcoreapplication.h> |
| #include <qmath.h> |
| #include <QtCore/qdebug.h> |
| |
| #include <mutex> |
| |
| #include <float.h> |
| #include <evcode.h> |
| |
| QT_BEGIN_NAMESPACE |
| |
| const static MFRatio g_DefaultFrameRate = { 30, 1 }; |
| static const DWORD SCHEDULER_TIMEOUT = 5000; |
| static const MFTIME ONE_SECOND = 10000000; |
| static const LONG ONE_MSEC = 1000; |
| |
| // Function declarations. |
| static HRESULT setDesiredSampleTime(IMFSample *sample, const LONGLONG& hnsSampleTime, const LONGLONG& hnsDuration); |
| static HRESULT clearDesiredSampleTime(IMFSample *sample); |
| static HRESULT setMixerSourceRect(IMFTransform *mixer, const MFVideoNormalizedRect& nrcSource); |
| static QVideoFrame::PixelFormat pixelFormatFromMediaType(IMFMediaType *type); |
| |
| static inline LONG MFTimeToMsec(const LONGLONG& time) |
| { |
| return (LONG)(time / (ONE_SECOND / ONE_MSEC)); |
| } |
| |
| bool qt_evr_setCustomPresenter(IUnknown *evr, EVRCustomPresenter *presenter) |
| { |
| if (!evr || !presenter) |
| return false; |
| |
| HRESULT result = E_FAIL; |
| |
| IMFVideoRenderer *renderer = NULL; |
| if (SUCCEEDED(evr->QueryInterface(IID_PPV_ARGS(&renderer)))) { |
| result = renderer->InitializeRenderer(NULL, presenter); |
| renderer->Release(); |
| } |
| |
| return result == S_OK; |
| } |
| |
| class PresentSampleEvent : public QEvent |
| { |
| public: |
| PresentSampleEvent(IMFSample *sample) |
| : QEvent(QEvent::Type(EVRCustomPresenter::PresentSample)) |
| , m_sample(sample) |
| { |
| if (m_sample) |
| m_sample->AddRef(); |
| } |
| |
| ~PresentSampleEvent() override |
| { |
| if (m_sample) |
| m_sample->Release(); |
| } |
| |
| IMFSample *sample() const { return m_sample; } |
| |
| private: |
| IMFSample *m_sample; |
| }; |
| |
| Scheduler::Scheduler(EVRCustomPresenter *presenter) |
| : m_presenter(presenter) |
| , m_clock(NULL) |
| , m_threadID(0) |
| , m_schedulerThread(0) |
| , m_threadReadyEvent(0) |
| , m_flushEvent(0) |
| , m_playbackRate(1.0f) |
| , m_perFrameInterval(0) |
| , m_perFrame_1_4th(0) |
| , m_lastSampleTime(0) |
| { |
| } |
| |
| Scheduler::~Scheduler() |
| { |
| qt_evr_safe_release(&m_clock); |
| for (int i = 0; i < m_scheduledSamples.size(); ++i) |
| m_scheduledSamples[i]->Release(); |
| m_scheduledSamples.clear(); |
| } |
| |
| void Scheduler::setFrameRate(const MFRatio& fps) |
| { |
| UINT64 AvgTimePerFrame = 0; |
| |
| // Convert to a duration. |
| MFFrameRateToAverageTimePerFrame(fps.Numerator, fps.Denominator, &AvgTimePerFrame); |
| |
| m_perFrameInterval = (MFTIME)AvgTimePerFrame; |
| |
| // Calculate 1/4th of this value, because we use it frequently. |
| m_perFrame_1_4th = m_perFrameInterval / 4; |
| } |
| |
| HRESULT Scheduler::startScheduler(IMFClock *clock) |
| { |
| if (m_schedulerThread) |
| return E_UNEXPECTED; |
| |
| HRESULT hr = S_OK; |
| DWORD dwID = 0; |
| HANDLE hObjects[2]; |
| DWORD dwWait = 0; |
| |
| if (m_clock) |
| m_clock->Release(); |
| m_clock = clock; |
| if (m_clock) |
| m_clock->AddRef(); |
| |
| // Set a high the timer resolution (ie, short timer period). |
| timeBeginPeriod(1); |
| |
| // Create an event to wait for the thread to start. |
| m_threadReadyEvent = CreateEvent(NULL, FALSE, FALSE, NULL); |
| if (!m_threadReadyEvent) { |
| hr = HRESULT_FROM_WIN32(GetLastError()); |
| goto done; |
| } |
| |
| // Create an event to wait for flush commands to complete. |
| m_flushEvent = CreateEvent(NULL, FALSE, FALSE, NULL); |
| if (!m_flushEvent) { |
| hr = HRESULT_FROM_WIN32(GetLastError()); |
| goto done; |
| } |
| |
| // Create the scheduler thread. |
| m_schedulerThread = CreateThread(NULL, 0, schedulerThreadProc, (LPVOID)this, 0, &dwID); |
| if (!m_schedulerThread) { |
| hr = HRESULT_FROM_WIN32(GetLastError()); |
| goto done; |
| } |
| |
| // Wait for the thread to signal the "thread ready" event. |
| hObjects[0] = m_threadReadyEvent; |
| hObjects[1] = m_schedulerThread; |
| dwWait = WaitForMultipleObjects(2, hObjects, FALSE, INFINITE); // Wait for EITHER of these handles. |
| if (WAIT_OBJECT_0 != dwWait) { |
| // The thread terminated early for some reason. This is an error condition. |
| CloseHandle(m_schedulerThread); |
| m_schedulerThread = NULL; |
| |
| hr = E_UNEXPECTED; |
| goto done; |
| } |
| |
| m_threadID = dwID; |
| |
| done: |
| // Regardless success/failure, we are done using the "thread ready" event. |
| if (m_threadReadyEvent) { |
| CloseHandle(m_threadReadyEvent); |
| m_threadReadyEvent = NULL; |
| } |
| return hr; |
| } |
| |
| HRESULT Scheduler::stopScheduler() |
| { |
| if (!m_schedulerThread) |
| return S_OK; |
| |
| // Ask the scheduler thread to exit. |
| PostThreadMessage(m_threadID, Terminate, 0, 0); |
| |
| // Wait for the thread to exit. |
| WaitForSingleObject(m_schedulerThread, INFINITE); |
| |
| // Close handles. |
| CloseHandle(m_schedulerThread); |
| m_schedulerThread = NULL; |
| |
| CloseHandle(m_flushEvent); |
| m_flushEvent = NULL; |
| |
| // Discard samples. |
| m_mutex.lock(); |
| for (int i = 0; i < m_scheduledSamples.size(); ++i) |
| m_scheduledSamples[i]->Release(); |
| m_scheduledSamples.clear(); |
| m_mutex.unlock(); |
| |
| // Restore the timer resolution. |
| timeEndPeriod(1); |
| |
| return S_OK; |
| } |
| |
| HRESULT Scheduler::flush() |
| { |
| if (m_schedulerThread) { |
| // Ask the scheduler thread to flush. |
| PostThreadMessage(m_threadID, Flush, 0 , 0); |
| |
| // Wait for the scheduler thread to signal the flush event, |
| // OR for the thread to terminate. |
| HANDLE objects[] = { m_flushEvent, m_schedulerThread }; |
| |
| WaitForMultipleObjects(ARRAYSIZE(objects), objects, FALSE, SCHEDULER_TIMEOUT); |
| } |
| |
| return S_OK; |
| } |
| |
| bool Scheduler::areSamplesScheduled() |
| { |
| QMutexLocker locker(&m_mutex); |
| return m_scheduledSamples.count() > 0; |
| } |
| |
| HRESULT Scheduler::scheduleSample(IMFSample *sample, bool presentNow) |
| { |
| if (!m_schedulerThread) |
| return MF_E_NOT_INITIALIZED; |
| |
| HRESULT hr = S_OK; |
| DWORD dwExitCode = 0; |
| |
| GetExitCodeThread(m_schedulerThread, &dwExitCode); |
| if (dwExitCode != STILL_ACTIVE) |
| return E_FAIL; |
| |
| if (presentNow || !m_clock) { |
| m_presenter->presentSample(sample); |
| } else { |
| // Queue the sample and ask the scheduler thread to wake up. |
| m_mutex.lock(); |
| sample->AddRef(); |
| m_scheduledSamples.enqueue(sample); |
| m_mutex.unlock(); |
| |
| if (SUCCEEDED(hr)) |
| PostThreadMessage(m_threadID, Schedule, 0, 0); |
| } |
| |
| return hr; |
| } |
| |
| HRESULT Scheduler::processSamplesInQueue(LONG *nextSleep) |
| { |
| HRESULT hr = S_OK; |
| LONG wait = 0; |
| IMFSample *sample = NULL; |
| |
| // Process samples until the queue is empty or until the wait time > 0. |
| while (!m_scheduledSamples.isEmpty()) { |
| m_mutex.lock(); |
| sample = m_scheduledSamples.dequeue(); |
| m_mutex.unlock(); |
| |
| // Process the next sample in the queue. If the sample is not ready |
| // for presentation. the value returned in wait is > 0, which |
| // means the scheduler should sleep for that amount of time. |
| |
| hr = processSample(sample, &wait); |
| qt_evr_safe_release(&sample); |
| |
| if (FAILED(hr) || wait > 0) |
| break; |
| } |
| |
| // If the wait time is zero, it means we stopped because the queue is |
| // empty (or an error occurred). Set the wait time to infinite; this will |
| // make the scheduler thread sleep until it gets another thread message. |
| if (wait == 0) |
| wait = INFINITE; |
| |
| *nextSleep = wait; |
| return hr; |
| } |
| |
| HRESULT Scheduler::processSample(IMFSample *sample, LONG *pNextSleep) |
| { |
| HRESULT hr = S_OK; |
| |
| LONGLONG hnsPresentationTime = 0; |
| LONGLONG hnsTimeNow = 0; |
| MFTIME hnsSystemTime = 0; |
| |
| bool presentNow = true; |
| LONG nextSleep = 0; |
| |
| if (m_clock) { |
| // Get the sample's time stamp. It is valid for a sample to |
| // have no time stamp. |
| hr = sample->GetSampleTime(&hnsPresentationTime); |
| |
| // Get the clock time. (But if the sample does not have a time stamp, |
| // we don't need the clock time.) |
| if (SUCCEEDED(hr)) |
| hr = m_clock->GetCorrelatedTime(0, &hnsTimeNow, &hnsSystemTime); |
| |
| // Calculate the time until the sample's presentation time. |
| // A negative value means the sample is late. |
| LONGLONG hnsDelta = hnsPresentationTime - hnsTimeNow; |
| if (m_playbackRate < 0) { |
| // For reverse playback, the clock runs backward. Therefore, the |
| // delta is reversed. |
| hnsDelta = - hnsDelta; |
| } |
| |
| if (hnsDelta < - m_perFrame_1_4th) { |
| // This sample is late. |
| presentNow = true; |
| } else if (hnsDelta > (3 * m_perFrame_1_4th)) { |
| // This sample is still too early. Go to sleep. |
| nextSleep = MFTimeToMsec(hnsDelta - (3 * m_perFrame_1_4th)); |
| |
| // Adjust the sleep time for the clock rate. (The presentation clock runs |
| // at m_fRate, but sleeping uses the system clock.) |
| if (m_playbackRate != 0) |
| nextSleep = (LONG)(nextSleep / qFabs(m_playbackRate)); |
| |
| // Don't present yet. |
| presentNow = false; |
| } |
| } |
| |
| if (presentNow) { |
| m_presenter->presentSample(sample); |
| } else { |
| // The sample is not ready yet. Return it to the queue. |
| m_mutex.lock(); |
| sample->AddRef(); |
| m_scheduledSamples.prepend(sample); |
| m_mutex.unlock(); |
| } |
| |
| *pNextSleep = nextSleep; |
| |
| return hr; |
| } |
| |
| DWORD WINAPI Scheduler::schedulerThreadProc(LPVOID parameter) |
| { |
| Scheduler* scheduler = reinterpret_cast<Scheduler*>(parameter); |
| if (!scheduler) |
| return -1; |
| return scheduler->schedulerThreadProcPrivate(); |
| } |
| |
| DWORD Scheduler::schedulerThreadProcPrivate() |
| { |
| HRESULT hr = S_OK; |
| MSG msg; |
| LONG wait = INFINITE; |
| bool exitThread = false; |
| |
| // Force the system to create a message queue for this thread. |
| // (See MSDN documentation for PostThreadMessage.) |
| PeekMessage(&msg, NULL, WM_USER, WM_USER, PM_NOREMOVE); |
| |
| // Signal to the scheduler that the thread is ready. |
| SetEvent(m_threadReadyEvent); |
| |
| while (!exitThread) { |
| // Wait for a thread message OR until the wait time expires. |
| DWORD result = MsgWaitForMultipleObjects(0, NULL, FALSE, wait, QS_POSTMESSAGE); |
| |
| if (result == WAIT_TIMEOUT) { |
| // If we timed out, then process the samples in the queue |
| hr = processSamplesInQueue(&wait); |
| if (FAILED(hr)) |
| exitThread = true; |
| } |
| |
| while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { |
| bool processSamples = true; |
| |
| switch (msg.message) { |
| case Terminate: |
| exitThread = true; |
| break; |
| case Flush: |
| // Flushing: Clear the sample queue and set the event. |
| m_mutex.lock(); |
| for (int i = 0; i < m_scheduledSamples.size(); ++i) |
| m_scheduledSamples[i]->Release(); |
| m_scheduledSamples.clear(); |
| m_mutex.unlock(); |
| wait = INFINITE; |
| SetEvent(m_flushEvent); |
| break; |
| case Schedule: |
| // Process as many samples as we can. |
| if (processSamples) { |
| hr = processSamplesInQueue(&wait); |
| if (FAILED(hr)) |
| exitThread = true; |
| processSamples = (wait != (LONG)INFINITE); |
| } |
| break; |
| } |
| } |
| |
| } |
| |
| return (SUCCEEDED(hr) ? 0 : 1); |
| } |
| |
| |
| SamplePool::SamplePool() |
| : m_initialized(false) |
| { |
| } |
| |
| SamplePool::~SamplePool() |
| { |
| clear(); |
| } |
| |
| HRESULT SamplePool::getSample(IMFSample **sample) |
| { |
| QMutexLocker locker(&m_mutex); |
| |
| if (!m_initialized) |
| return MF_E_NOT_INITIALIZED; |
| |
| if (m_videoSampleQueue.isEmpty()) |
| return MF_E_SAMPLEALLOCATOR_EMPTY; |
| |
| // Get a sample from the allocated queue. |
| |
| // It doesn't matter if we pull them from the head or tail of the list, |
| // but when we get it back, we want to re-insert it onto the opposite end. |
| // (see ReturnSample) |
| |
| IMFSample *taken = m_videoSampleQueue.takeFirst(); |
| |
| // Give the sample to the caller. |
| *sample = taken; |
| (*sample)->AddRef(); |
| |
| taken->Release(); |
| |
| return S_OK; |
| } |
| |
| HRESULT SamplePool::returnSample(IMFSample *sample) |
| { |
| QMutexLocker locker(&m_mutex); |
| |
| if (!m_initialized) |
| return MF_E_NOT_INITIALIZED; |
| |
| m_videoSampleQueue.append(sample); |
| sample->AddRef(); |
| |
| return S_OK; |
| } |
| |
| HRESULT SamplePool::initialize(QList<IMFSample*> &samples) |
| { |
| QMutexLocker locker(&m_mutex); |
| |
| if (m_initialized) |
| return MF_E_INVALIDREQUEST; |
| |
| // Move these samples into our allocated queue. |
| for (auto sample : qAsConst(samples)) { |
| sample->AddRef(); |
| m_videoSampleQueue.append(sample); |
| } |
| |
| m_initialized = true; |
| |
| for (auto sample : qAsConst(samples)) |
| sample->Release(); |
| samples.clear(); |
| return S_OK; |
| } |
| |
| HRESULT SamplePool::clear() |
| { |
| QMutexLocker locker(&m_mutex); |
| |
| for (auto sample : qAsConst(m_videoSampleQueue)) |
| sample->Release(); |
| m_videoSampleQueue.clear(); |
| m_initialized = false; |
| |
| return S_OK; |
| } |
| |
| |
| EVRCustomPresenter::EVRCustomPresenter(QAbstractVideoSurface *surface) |
| : QObject() |
| , m_sampleFreeCB(this, &EVRCustomPresenter::onSampleFree) |
| , m_refCount(1) |
| , m_renderState(RenderShutdown) |
| , m_scheduler(this) |
| , m_tokenCounter(0) |
| , m_sampleNotify(false) |
| , m_repaint(false) |
| , m_prerolled(false) |
| , m_endStreaming(false) |
| , m_playbackRate(1.0f) |
| , m_presentEngine(new D3DPresentEngine) |
| , m_clock(0) |
| , m_mixer(0) |
| , m_mediaEventSink(0) |
| , m_mediaType(0) |
| , m_surface(0) |
| , m_canRenderToSurface(false) |
| , m_positionOffset(0) |
| { |
| // Initial source rectangle = (0,0,1,1) |
| m_sourceRect.top = 0; |
| m_sourceRect.left = 0; |
| m_sourceRect.bottom = 1; |
| m_sourceRect.right = 1; |
| |
| setSurface(surface); |
| } |
| |
| EVRCustomPresenter::~EVRCustomPresenter() |
| { |
| m_scheduler.flush(); |
| m_scheduler.stopScheduler(); |
| m_samplePool.clear(); |
| |
| qt_evr_safe_release(&m_clock); |
| qt_evr_safe_release(&m_mixer); |
| qt_evr_safe_release(&m_mediaEventSink); |
| qt_evr_safe_release(&m_mediaType); |
| |
| delete m_presentEngine; |
| } |
| |
| HRESULT EVRCustomPresenter::QueryInterface(REFIID riid, void ** ppvObject) |
| { |
| if (!ppvObject) |
| return E_POINTER; |
| if (riid == IID_IMFGetService) { |
| *ppvObject = static_cast<IMFGetService*>(this); |
| } else if (riid == IID_IMFTopologyServiceLookupClient) { |
| *ppvObject = static_cast<IMFTopologyServiceLookupClient*>(this); |
| } else if (riid == IID_IMFVideoDeviceID) { |
| *ppvObject = static_cast<IMFVideoDeviceID*>(this); |
| } else if (riid == IID_IMFVideoPresenter) { |
| *ppvObject = static_cast<IMFVideoPresenter*>(this); |
| } else if (riid == IID_IMFRateSupport) { |
| *ppvObject = static_cast<IMFRateSupport*>(this); |
| } else if (riid == IID_IUnknown) { |
| *ppvObject = static_cast<IUnknown*>(static_cast<IMFGetService*>(this)); |
| } else if (riid == IID_IMFClockStateSink) { |
| *ppvObject = static_cast<IMFClockStateSink*>(this); |
| } else { |
| *ppvObject = NULL; |
| return E_NOINTERFACE; |
| } |
| AddRef(); |
| return S_OK; |
| } |
| |
| ULONG EVRCustomPresenter::AddRef() |
| { |
| return InterlockedIncrement(&m_refCount); |
| } |
| |
| ULONG EVRCustomPresenter::Release() |
| { |
| ULONG uCount = InterlockedDecrement(&m_refCount); |
| if (uCount == 0) |
| delete this; |
| return uCount; |
| } |
| |
| HRESULT EVRCustomPresenter::GetService(REFGUID guidService, REFIID riid, LPVOID *ppvObject) |
| { |
| HRESULT hr = S_OK; |
| |
| if (!ppvObject) |
| return E_POINTER; |
| |
| // The only service GUID that we support is MR_VIDEO_RENDER_SERVICE. |
| if (guidService != mr_VIDEO_RENDER_SERVICE) |
| return MF_E_UNSUPPORTED_SERVICE; |
| |
| // First try to get the service interface from the D3DPresentEngine object. |
| hr = m_presentEngine->getService(guidService, riid, ppvObject); |
| if (FAILED(hr)) |
| // Next, check if this object supports the interface. |
| hr = QueryInterface(riid, ppvObject); |
| |
| return hr; |
| } |
| |
| HRESULT EVRCustomPresenter::GetDeviceID(IID* deviceID) |
| { |
| if (!deviceID) |
| return E_POINTER; |
| |
| *deviceID = iid_IDirect3DDevice9; |
| |
| return S_OK; |
| } |
| |
| HRESULT EVRCustomPresenter::InitServicePointers(IMFTopologyServiceLookup *lookup) |
| { |
| if (!lookup) |
| return E_POINTER; |
| |
| HRESULT hr = S_OK; |
| DWORD objectCount = 0; |
| |
| const std::lock_guard<QRecursiveMutex> locker(m_mutex); |
| |
| // Do not allow initializing when playing or paused. |
| if (isActive()) |
| return MF_E_INVALIDREQUEST; |
| |
| qt_evr_safe_release(&m_clock); |
| qt_evr_safe_release(&m_mixer); |
| qt_evr_safe_release(&m_mediaEventSink); |
| |
| // Ask for the clock. Optional, because the EVR might not have a clock. |
| objectCount = 1; |
| |
| lookup->LookupService(MF_SERVICE_LOOKUP_GLOBAL, 0, |
| mr_VIDEO_RENDER_SERVICE, IID_PPV_ARGS(&m_clock), |
| &objectCount |
| ); |
| |
| // Ask for the mixer. (Required.) |
| objectCount = 1; |
| |
| hr = lookup->LookupService(MF_SERVICE_LOOKUP_GLOBAL, 0, |
| mr_VIDEO_MIXER_SERVICE, IID_PPV_ARGS(&m_mixer), |
| &objectCount |
| ); |
| |
| if (FAILED(hr)) |
| return hr; |
| |
| // Make sure that we can work with this mixer. |
| hr = configureMixer(m_mixer); |
| if (FAILED(hr)) |
| return hr; |
| |
| // Ask for the EVR's event-sink interface. (Required.) |
| objectCount = 1; |
| |
| hr = lookup->LookupService(MF_SERVICE_LOOKUP_GLOBAL, 0, |
| mr_VIDEO_RENDER_SERVICE, IID_PPV_ARGS(&m_mediaEventSink), |
| &objectCount |
| ); |
| |
| if (SUCCEEDED(hr)) |
| m_renderState = RenderStopped; |
| |
| return hr; |
| } |
| |
| HRESULT EVRCustomPresenter::ReleaseServicePointers() |
| { |
| // Enter the shut-down state. |
| m_mutex.lock(); |
| |
| m_renderState = RenderShutdown; |
| |
| m_mutex.unlock(); |
| |
| // Flush any samples that were scheduled. |
| flush(); |
| |
| // Clear the media type and release related resources. |
| setMediaType(NULL); |
| |
| // Release all services that were acquired from InitServicePointers. |
| qt_evr_safe_release(&m_clock); |
| qt_evr_safe_release(&m_mixer); |
| qt_evr_safe_release(&m_mediaEventSink); |
| |
| return S_OK; |
| } |
| |
| bool EVRCustomPresenter::isValid() const |
| { |
| return m_presentEngine->isValid() && m_canRenderToSurface; |
| } |
| |
| HRESULT EVRCustomPresenter::ProcessMessage(MFVP_MESSAGE_TYPE message, ULONG_PTR param) |
| { |
| HRESULT hr = S_OK; |
| |
| const std::lock_guard<QRecursiveMutex> locker(m_mutex); |
| |
| hr = checkShutdown(); |
| if (FAILED(hr)) |
| return hr; |
| |
| switch (message) { |
| // Flush all pending samples. |
| case MFVP_MESSAGE_FLUSH: |
| hr = flush(); |
| break; |
| |
| // Renegotiate the media type with the mixer. |
| case MFVP_MESSAGE_INVALIDATEMEDIATYPE: |
| hr = renegotiateMediaType(); |
| break; |
| |
| // The mixer received a new input sample. |
| case MFVP_MESSAGE_PROCESSINPUTNOTIFY: |
| hr = processInputNotify(); |
| break; |
| |
| // Streaming is about to start. |
| case MFVP_MESSAGE_BEGINSTREAMING: |
| hr = beginStreaming(); |
| break; |
| |
| // Streaming has ended. (The EVR has stopped.) |
| case MFVP_MESSAGE_ENDSTREAMING: |
| hr = endStreaming(); |
| break; |
| |
| // All input streams have ended. |
| case MFVP_MESSAGE_ENDOFSTREAM: |
| // Set the EOS flag. |
| m_endStreaming = true; |
| // Check if it's time to send the EC_COMPLETE event to the EVR. |
| hr = checkEndOfStream(); |
| break; |
| |
| // Frame-stepping is starting. |
| case MFVP_MESSAGE_STEP: |
| hr = prepareFrameStep(DWORD(param)); |
| break; |
| |
| // Cancels frame-stepping. |
| case MFVP_MESSAGE_CANCELSTEP: |
| hr = cancelFrameStep(); |
| break; |
| |
| default: |
| hr = E_INVALIDARG; // Unknown message. This case should never occur. |
| break; |
| } |
| |
| return hr; |
| } |
| |
| HRESULT EVRCustomPresenter::GetCurrentMediaType(IMFVideoMediaType **mediaType) |
| { |
| HRESULT hr = S_OK; |
| |
| if (!mediaType) |
| return E_POINTER; |
| |
| *mediaType = NULL; |
| |
| const std::lock_guard<QRecursiveMutex> locker(m_mutex); |
| |
| hr = checkShutdown(); |
| if (FAILED(hr)) |
| return hr; |
| |
| if (!m_mediaType) |
| return MF_E_NOT_INITIALIZED; |
| |
| return m_mediaType->QueryInterface(IID_PPV_ARGS(mediaType)); |
| } |
| |
| HRESULT EVRCustomPresenter::OnClockStart(MFTIME, LONGLONG clockStartOffset) |
| { |
| const std::lock_guard<QRecursiveMutex> locker(m_mutex); |
| |
| // We cannot start after shutdown. |
| HRESULT hr = checkShutdown(); |
| if (FAILED(hr)) |
| return hr; |
| |
| // Check if the clock is already active (not stopped). |
| if (isActive()) { |
| m_renderState = RenderStarted; |
| |
| // If the clock position changes while the clock is active, it |
| // is a seek request. We need to flush all pending samples. |
| if (clockStartOffset != PRESENTATION_CURRENT_POSITION) |
| flush(); |
| } else { |
| m_renderState = RenderStarted; |
| |
| // The clock has started from the stopped state. |
| |
| // Possibly we are in the middle of frame-stepping OR have samples waiting |
| // in the frame-step queue. Deal with these two cases first: |
| hr = startFrameStep(); |
| if (FAILED(hr)) |
| return hr; |
| } |
| |
| // Now try to get new output samples from the mixer. |
| processOutputLoop(); |
| |
| return hr; |
| } |
| |
| HRESULT EVRCustomPresenter::OnClockRestart(MFTIME) |
| { |
| const std::lock_guard<QRecursiveMutex> locker(m_mutex); |
| |
| HRESULT hr = checkShutdown(); |
| if (FAILED(hr)) |
| return hr; |
| |
| // The EVR calls OnClockRestart only while paused. |
| |
| m_renderState = RenderStarted; |
| |
| // Possibly we are in the middle of frame-stepping OR we have samples waiting |
| // in the frame-step queue. Deal with these two cases first: |
| hr = startFrameStep(); |
| if (FAILED(hr)) |
| return hr; |
| |
| // Now resume the presentation loop. |
| processOutputLoop(); |
| |
| return hr; |
| } |
| |
| HRESULT EVRCustomPresenter::OnClockStop(MFTIME) |
| { |
| const std::lock_guard<QRecursiveMutex> locker(m_mutex); |
| |
| HRESULT hr = checkShutdown(); |
| if (FAILED(hr)) |
| return hr; |
| |
| if (m_renderState != RenderStopped) { |
| m_renderState = RenderStopped; |
| flush(); |
| |
| // If we are in the middle of frame-stepping, cancel it now. |
| if (m_frameStep.state != FrameStepNone) |
| cancelFrameStep(); |
| } |
| |
| return S_OK; |
| } |
| |
| HRESULT EVRCustomPresenter::OnClockPause(MFTIME) |
| { |
| const std::lock_guard<QRecursiveMutex> locker(m_mutex); |
| |
| // We cannot pause the clock after shutdown. |
| HRESULT hr = checkShutdown(); |
| |
| if (SUCCEEDED(hr)) |
| m_renderState = RenderPaused; |
| |
| return hr; |
| } |
| |
| HRESULT EVRCustomPresenter::OnClockSetRate(MFTIME, float rate) |
| { |
| // Note: |
| // The presenter reports its maximum rate through the IMFRateSupport interface. |
| // Here, we assume that the EVR honors the maximum rate. |
| |
| const std::lock_guard<QRecursiveMutex> locker(m_mutex); |
| |
| HRESULT hr = checkShutdown(); |
| if (FAILED(hr)) |
| return hr; |
| |
| // If the rate is changing from zero (scrubbing) to non-zero, cancel the |
| // frame-step operation. |
| if ((m_playbackRate == 0.0f) && (rate != 0.0f)) { |
| cancelFrameStep(); |
| for (auto sample : qAsConst(m_frameStep.samples)) |
| sample->Release(); |
| m_frameStep.samples.clear(); |
| } |
| |
| m_playbackRate = rate; |
| |
| // Tell the scheduler about the new rate. |
| m_scheduler.setClockRate(rate); |
| |
| return S_OK; |
| } |
| |
| HRESULT EVRCustomPresenter::GetSlowestRate(MFRATE_DIRECTION, BOOL, float *rate) |
| { |
| if (!rate) |
| return E_POINTER; |
| |
| const std::lock_guard<QRecursiveMutex> locker(m_mutex); |
| |
| HRESULT hr = checkShutdown(); |
| |
| if (SUCCEEDED(hr)) { |
| // There is no minimum playback rate, so the minimum is zero. |
| *rate = 0; |
| } |
| |
| return S_OK; |
| } |
| |
| HRESULT EVRCustomPresenter::GetFastestRate(MFRATE_DIRECTION direction, BOOL thin, float *rate) |
| { |
| if (!rate) |
| return E_POINTER; |
| |
| const std::lock_guard<QRecursiveMutex> locker(m_mutex); |
| |
| float maxRate = 0.0f; |
| |
| HRESULT hr = checkShutdown(); |
| if (FAILED(hr)) |
| return hr; |
| |
| // Get the maximum *forward* rate. |
| maxRate = getMaxRate(thin); |
| |
| // For reverse playback, it's the negative of maxRate. |
| if (direction == MFRATE_REVERSE) |
| maxRate = -maxRate; |
| |
| *rate = maxRate; |
| |
| return S_OK; |
| } |
| |
| HRESULT EVRCustomPresenter::IsRateSupported(BOOL thin, float rate, float *nearestSupportedRate) |
| { |
| const std::lock_guard<QRecursiveMutex> locker(m_mutex); |
| |
| float maxRate = 0.0f; |
| float nearestRate = rate; // If we support rate, that is the nearest. |
| |
| HRESULT hr = checkShutdown(); |
| if (FAILED(hr)) |
| return hr; |
| |
| // Find the maximum forward rate. |
| // Note: We have no minimum rate (that is, we support anything down to 0). |
| maxRate = getMaxRate(thin); |
| |
| if (qFabs(rate) > maxRate) { |
| // The (absolute) requested rate exceeds the maximum rate. |
| hr = MF_E_UNSUPPORTED_RATE; |
| |
| // The nearest supported rate is maxRate. |
| nearestRate = maxRate; |
| if (rate < 0) { |
| // Negative for reverse playback. |
| nearestRate = -nearestRate; |
| } |
| } |
| |
| // Return the nearest supported rate. |
| if (nearestSupportedRate) |
| *nearestSupportedRate = nearestRate; |
| |
| return hr; |
| } |
| |
| void EVRCustomPresenter::supportedFormatsChanged() |
| { |
| const std::lock_guard<QRecursiveMutex> locker(m_mutex); |
| |
| m_canRenderToSurface = false; |
| m_presentEngine->setHint(D3DPresentEngine::RenderToTexture, false); |
| |
| // check if we can render to the surface (compatible formats) |
| if (m_surface) { |
| QList<QVideoFrame::PixelFormat> formats = m_surface->supportedPixelFormats(QAbstractVideoBuffer::GLTextureHandle); |
| if (m_presentEngine->supportsTextureRendering() && formats.contains(QVideoFrame::Format_RGB32)) { |
| m_presentEngine->setHint(D3DPresentEngine::RenderToTexture, true); |
| m_canRenderToSurface = true; |
| } else { |
| formats = m_surface->supportedPixelFormats(QAbstractVideoBuffer::NoHandle); |
| for (QVideoFrame::PixelFormat format : qAsConst(formats)) { |
| if (SUCCEEDED(m_presentEngine->checkFormat(qt_evr_D3DFormatFromPixelFormat(format)))) { |
| m_canRenderToSurface = true; |
| break; |
| } |
| } |
| } |
| } |
| |
| // TODO: if media type already set, renegotiate? |
| } |
| |
| void EVRCustomPresenter::setSurface(QAbstractVideoSurface *surface) |
| { |
| m_mutex.lock(); |
| |
| if (m_surface) { |
| disconnect(m_surface, &QAbstractVideoSurface::supportedFormatsChanged, |
| this, &EVRCustomPresenter::supportedFormatsChanged); |
| } |
| |
| m_surface = surface; |
| |
| if (m_surface) { |
| connect(m_surface, &QAbstractVideoSurface::supportedFormatsChanged, |
| this, &EVRCustomPresenter::supportedFormatsChanged); |
| } |
| |
| m_mutex.unlock(); |
| |
| supportedFormatsChanged(); |
| } |
| |
| HRESULT EVRCustomPresenter::configureMixer(IMFTransform *mixer) |
| { |
| // Set the zoom rectangle (ie, the source clipping rectangle). |
| return setMixerSourceRect(mixer, m_sourceRect); |
| } |
| |
| HRESULT EVRCustomPresenter::renegotiateMediaType() |
| { |
| HRESULT hr = S_OK; |
| bool foundMediaType = false; |
| |
| IMFMediaType *mixerType = NULL; |
| IMFMediaType *optimalType = NULL; |
| |
| if (!m_mixer) |
| return MF_E_INVALIDREQUEST; |
| |
| // Loop through all of the mixer's proposed output types. |
| DWORD typeIndex = 0; |
| while (!foundMediaType && (hr != MF_E_NO_MORE_TYPES)) { |
| qt_evr_safe_release(&mixerType); |
| qt_evr_safe_release(&optimalType); |
| |
| // Step 1. Get the next media type supported by mixer. |
| hr = m_mixer->GetOutputAvailableType(0, typeIndex++, &mixerType); |
| if (FAILED(hr)) |
| break; |
| |
| // From now on, if anything in this loop fails, try the next type, |
| // until we succeed or the mixer runs out of types. |
| |
| // Step 2. Check if we support this media type. |
| if (SUCCEEDED(hr)) |
| hr = isMediaTypeSupported(mixerType); |
| |
| // Step 3. Adjust the mixer's type to match our requirements. |
| if (SUCCEEDED(hr)) |
| hr = createOptimalVideoType(mixerType, &optimalType); |
| |
| // Step 4. Check if the mixer will accept this media type. |
| if (SUCCEEDED(hr)) |
| hr = m_mixer->SetOutputType(0, optimalType, MFT_SET_TYPE_TEST_ONLY); |
| |
| // Step 5. Try to set the media type on ourselves. |
| if (SUCCEEDED(hr)) |
| hr = setMediaType(optimalType); |
| |
| // Step 6. Set output media type on mixer. |
| if (SUCCEEDED(hr)) { |
| hr = m_mixer->SetOutputType(0, optimalType, 0); |
| |
| // If something went wrong, clear the media type. |
| if (FAILED(hr)) |
| setMediaType(NULL); |
| } |
| |
| if (SUCCEEDED(hr)) |
| foundMediaType = true; |
| } |
| |
| qt_evr_safe_release(&mixerType); |
| qt_evr_safe_release(&optimalType); |
| |
| return hr; |
| } |
| |
| HRESULT EVRCustomPresenter::flush() |
| { |
| m_prerolled = false; |
| |
| // The scheduler might have samples that are waiting for |
| // their presentation time. Tell the scheduler to flush. |
| |
| // This call blocks until the scheduler threads discards all scheduled samples. |
| m_scheduler.flush(); |
| |
| // Flush the frame-step queue. |
| for (auto sample : qAsConst(m_frameStep.samples)) |
| sample->Release(); |
| m_frameStep.samples.clear(); |
| |
| if (m_renderState == RenderStopped && m_surface && m_surface->isActive()) { |
| // Repaint with black. |
| presentSample(NULL); |
| } |
| |
| return S_OK; |
| } |
| |
| HRESULT EVRCustomPresenter::processInputNotify() |
| { |
| HRESULT hr = S_OK; |
| |
| // Set the flag that says the mixer has a new sample. |
| m_sampleNotify = true; |
| |
| if (!m_mediaType) { |
| // We don't have a valid media type yet. |
| hr = MF_E_TRANSFORM_TYPE_NOT_SET; |
| } else { |
| // Try to process an output sample. |
| processOutputLoop(); |
| } |
| return hr; |
| } |
| |
| HRESULT EVRCustomPresenter::beginStreaming() |
| { |
| HRESULT hr = S_OK; |
| |
| // Start the scheduler thread. |
| hr = m_scheduler.startScheduler(m_clock); |
| |
| return hr; |
| } |
| |
| HRESULT EVRCustomPresenter::endStreaming() |
| { |
| HRESULT hr = S_OK; |
| |
| // Stop the scheduler thread. |
| hr = m_scheduler.stopScheduler(); |
| |
| return hr; |
| } |
| |
| HRESULT EVRCustomPresenter::checkEndOfStream() |
| { |
| if (!m_endStreaming) { |
| // The EVR did not send the MFVP_MESSAGE_ENDOFSTREAM message. |
| return S_OK; |
| } |
| |
| if (m_sampleNotify) { |
| // The mixer still has input. |
| return S_OK; |
| } |
| |
| if (m_scheduler.areSamplesScheduled()) { |
| // Samples are still scheduled for rendering. |
| return S_OK; |
| } |
| |
| // Everything is complete. Now we can tell the EVR that we are done. |
| notifyEvent(EC_COMPLETE, (LONG_PTR)S_OK, 0); |
| m_endStreaming = false; |
| |
| stopSurface(); |
| return S_OK; |
| } |
| |
| HRESULT EVRCustomPresenter::prepareFrameStep(DWORD steps) |
| { |
| HRESULT hr = S_OK; |
| |
| // Cache the step count. |
| m_frameStep.steps += steps; |
| |
| // Set the frame-step state. |
| m_frameStep.state = FrameStepWaitingStart; |
| |
| // If the clock is are already running, we can start frame-stepping now. |
| // Otherwise, we will start when the clock starts. |
| if (m_renderState == RenderStarted) |
| hr = startFrameStep(); |
| |
| return hr; |
| } |
| |
| HRESULT EVRCustomPresenter::startFrameStep() |
| { |
| HRESULT hr = S_OK; |
| IMFSample *sample = NULL; |
| |
| if (m_frameStep.state == FrameStepWaitingStart) { |
| // We have a frame-step request, and are waiting for the clock to start. |
| // Set the state to "pending," which means we are waiting for samples. |
| m_frameStep.state = FrameStepPending; |
| |
| // If the frame-step queue already has samples, process them now. |
| while (!m_frameStep.samples.isEmpty() && (m_frameStep.state == FrameStepPending)) { |
| sample = m_frameStep.samples.takeFirst(); |
| |
| hr = deliverFrameStepSample(sample); |
| if (FAILED(hr)) |
| goto done; |
| |
| qt_evr_safe_release(&sample); |
| |
| // We break from this loop when: |
| // (a) the frame-step queue is empty, or |
| // (b) the frame-step operation is complete. |
| } |
| } else if (m_frameStep.state == FrameStepNone) { |
| // We are not frame stepping. Therefore, if the frame-step queue has samples, |
| // we need to process them normally. |
| while (!m_frameStep.samples.isEmpty()) { |
| sample = m_frameStep.samples.takeFirst(); |
| |
| hr = deliverSample(sample, false); |
| if (FAILED(hr)) |
| goto done; |
| |
| qt_evr_safe_release(&sample); |
| } |
| } |
| |
| done: |
| qt_evr_safe_release(&sample); |
| return hr; |
| } |
| |
| HRESULT EVRCustomPresenter::completeFrameStep(IMFSample *sample) |
| { |
| HRESULT hr = S_OK; |
| MFTIME sampleTime = 0; |
| MFTIME systemTime = 0; |
| |
| // Update our state. |
| m_frameStep.state = FrameStepComplete; |
| m_frameStep.sampleNoRef = 0; |
| |
| // Notify the EVR that the frame-step is complete. |
| notifyEvent(EC_STEP_COMPLETE, FALSE, 0); // FALSE = completed (not cancelled) |
| |
| // If we are scrubbing (rate == 0), also send the "scrub time" event. |
| if (isScrubbing()) { |
| // Get the time stamp from the sample. |
| hr = sample->GetSampleTime(&sampleTime); |
| if (FAILED(hr)) { |
| // No time stamp. Use the current presentation time. |
| if (m_clock) |
| m_clock->GetCorrelatedTime(0, &sampleTime, &systemTime); |
| |
| hr = S_OK; // (Not an error condition.) |
| } |
| |
| notifyEvent(EC_SCRUB_TIME, DWORD(sampleTime), DWORD(((sampleTime) >> 32) & 0xffffffff)); |
| } |
| return hr; |
| } |
| |
| HRESULT EVRCustomPresenter::cancelFrameStep() |
| { |
| FrameStepState oldState = m_frameStep.state; |
| |
| m_frameStep.state = FrameStepNone; |
| m_frameStep.steps = 0; |
| m_frameStep.sampleNoRef = 0; |
| // Don't clear the frame-step queue yet, because we might frame step again. |
| |
| if (oldState > FrameStepNone && oldState < FrameStepComplete) { |
| // We were in the middle of frame-stepping when it was cancelled. |
| // Notify the EVR. |
| notifyEvent(EC_STEP_COMPLETE, TRUE, 0); // TRUE = cancelled |
| } |
| return S_OK; |
| } |
| |
| HRESULT EVRCustomPresenter::createOptimalVideoType(IMFMediaType *proposedType, IMFMediaType **optimalType) |
| { |
| HRESULT hr = S_OK; |
| |
| RECT rcOutput; |
| ZeroMemory(&rcOutput, sizeof(rcOutput)); |
| |
| MFVideoArea displayArea; |
| ZeroMemory(&displayArea, sizeof(displayArea)); |
| |
| IMFMediaType *mtOptimal = NULL; |
| |
| UINT64 size; |
| int width; |
| int height; |
| |
| // Clone the proposed type. |
| |
| hr = MFCreateMediaType(&mtOptimal); |
| if (FAILED(hr)) |
| goto done; |
| |
| hr = proposedType->CopyAllItems(mtOptimal); |
| if (FAILED(hr)) |
| goto done; |
| |
| // Modify the new type. |
| |
| hr = proposedType->GetUINT64(MF_MT_FRAME_SIZE, &size); |
| width = int(HI32(size)); |
| height = int(LO32(size)); |
| rcOutput.left = 0; |
| rcOutput.top = 0; |
| rcOutput.right = width; |
| rcOutput.bottom = height; |
| |
| // Set the geometric aperture, and disable pan/scan. |
| displayArea = qt_evr_makeMFArea(0, 0, rcOutput.right, rcOutput.bottom); |
| |
| hr = mtOptimal->SetUINT32(MF_MT_PAN_SCAN_ENABLED, FALSE); |
| if (FAILED(hr)) |
| goto done; |
| |
| hr = mtOptimal->SetBlob(MF_MT_GEOMETRIC_APERTURE, reinterpret_cast<UINT8*>(&displayArea), |
| sizeof(displayArea)); |
| if (FAILED(hr)) |
| goto done; |
| |
| // Set the pan/scan aperture and the minimum display aperture. We don't care |
| // about them per se, but the mixer will reject the type if these exceed the |
| // frame dimentions. |
| hr = mtOptimal->SetBlob(MF_MT_PAN_SCAN_APERTURE, reinterpret_cast<UINT8*>(&displayArea), |
| sizeof(displayArea)); |
| if (FAILED(hr)) |
| goto done; |
| |
| hr = mtOptimal->SetBlob(MF_MT_MINIMUM_DISPLAY_APERTURE, reinterpret_cast<UINT8*>(&displayArea), |
| sizeof(displayArea)); |
| if (FAILED(hr)) |
| goto done; |
| |
| // Return the pointer to the caller. |
| *optimalType = mtOptimal; |
| (*optimalType)->AddRef(); |
| |
| done: |
| qt_evr_safe_release(&mtOptimal); |
| return hr; |
| |
| } |
| |
| HRESULT EVRCustomPresenter::setMediaType(IMFMediaType *mediaType) |
| { |
| // Note: mediaType can be NULL (to clear the type) |
| |
| // Clearing the media type is allowed in any state (including shutdown). |
| if (!mediaType) { |
| stopSurface(); |
| qt_evr_safe_release(&m_mediaType); |
| releaseResources(); |
| return S_OK; |
| } |
| |
| MFRatio fps = { 0, 0 }; |
| QList<IMFSample*> sampleQueue; |
| |
| // Cannot set the media type after shutdown. |
| HRESULT hr = checkShutdown(); |
| if (FAILED(hr)) |
| goto done; |
| |
| // Check if the new type is actually different. |
| // Note: This function safely handles NULL input parameters. |
| if (qt_evr_areMediaTypesEqual(m_mediaType, mediaType)) |
| goto done; // Nothing more to do. |
| |
| // We're really changing the type. First get rid of the old type. |
| qt_evr_safe_release(&m_mediaType); |
| releaseResources(); |
| |
| // Initialize the presenter engine with the new media type. |
| // The presenter engine allocates the samples. |
| |
| hr = m_presentEngine->createVideoSamples(mediaType, sampleQueue); |
| if (FAILED(hr)) |
| goto done; |
| |
| // Mark each sample with our token counter. If this batch of samples becomes |
| // invalid, we increment the counter, so that we know they should be discarded. |
| for (auto sample : qAsConst(sampleQueue)) { |
| hr = sample->SetUINT32(MFSamplePresenter_SampleCounter, m_tokenCounter); |
| if (FAILED(hr)) |
| goto done; |
| } |
| |
| // Add the samples to the sample pool. |
| hr = m_samplePool.initialize(sampleQueue); |
| if (FAILED(hr)) |
| goto done; |
| |
| // Set the frame rate on the scheduler. |
| if (SUCCEEDED(qt_evr_getFrameRate(mediaType, &fps)) && (fps.Numerator != 0) && (fps.Denominator != 0)) { |
| m_scheduler.setFrameRate(fps); |
| } else { |
| // NOTE: The mixer's proposed type might not have a frame rate, in which case |
| // we'll use an arbitrary default. (Although it's unlikely the video source |
| // does not have a frame rate.) |
| m_scheduler.setFrameRate(g_DefaultFrameRate); |
| } |
| |
| // Store the media type. |
| m_mediaType = mediaType; |
| m_mediaType->AddRef(); |
| |
| startSurface(); |
| |
| done: |
| if (FAILED(hr)) |
| releaseResources(); |
| return hr; |
| } |
| |
| HRESULT EVRCustomPresenter::isMediaTypeSupported(IMFMediaType *proposed) |
| { |
| D3DFORMAT d3dFormat = D3DFMT_UNKNOWN; |
| BOOL compressed = FALSE; |
| MFVideoInterlaceMode interlaceMode = MFVideoInterlace_Unknown; |
| MFVideoArea videoCropArea; |
| UINT32 width = 0, height = 0; |
| |
| // Validate the format. |
| HRESULT hr = qt_evr_getFourCC(proposed, reinterpret_cast<DWORD*>(&d3dFormat)); |
| if (FAILED(hr)) |
| return hr; |
| |
| QVideoFrame::PixelFormat pixelFormat = pixelFormatFromMediaType(proposed); |
| if (pixelFormat == QVideoFrame::Format_Invalid) |
| return MF_E_INVALIDMEDIATYPE; |
| |
| // When not rendering to texture, only accept pixel formats supported by the video surface |
| if (!m_presentEngine->isTextureRenderingEnabled() |
| && m_surface |
| && !m_surface->supportedPixelFormats().contains(pixelFormat)) { |
| return MF_E_INVALIDMEDIATYPE; |
| } |
| |
| // Reject compressed media types. |
| hr = proposed->IsCompressedFormat(&compressed); |
| if (FAILED(hr)) |
| return hr; |
| |
| if (compressed) |
| return MF_E_INVALIDMEDIATYPE; |
| |
| // The D3DPresentEngine checks whether surfaces can be created using this format |
| hr = m_presentEngine->checkFormat(d3dFormat); |
| if (FAILED(hr)) |
| return hr; |
| |
| // Reject interlaced formats. |
| hr = proposed->GetUINT32(MF_MT_INTERLACE_MODE, reinterpret_cast<UINT32*>(&interlaceMode)); |
| if (FAILED(hr)) |
| return hr; |
| |
| if (interlaceMode != MFVideoInterlace_Progressive) |
| return MF_E_INVALIDMEDIATYPE; |
| |
| hr = MFGetAttributeSize(proposed, MF_MT_FRAME_SIZE, &width, &height); |
| if (FAILED(hr)) |
| return hr; |
| |
| // Validate the various apertures (cropping regions) against the frame size. |
| // Any of these apertures may be unspecified in the media type, in which case |
| // we ignore it. We just want to reject invalid apertures. |
| |
| if (SUCCEEDED(proposed->GetBlob(MF_MT_PAN_SCAN_APERTURE, |
| reinterpret_cast<UINT8*>(&videoCropArea), |
| sizeof(videoCropArea), nullptr))) { |
| hr = qt_evr_validateVideoArea(videoCropArea, width, height); |
| } |
| if (SUCCEEDED(proposed->GetBlob(MF_MT_GEOMETRIC_APERTURE, |
| reinterpret_cast<UINT8*>(&videoCropArea), |
| sizeof(videoCropArea), nullptr))) { |
| hr = qt_evr_validateVideoArea(videoCropArea, width, height); |
| } |
| if (SUCCEEDED(proposed->GetBlob(MF_MT_MINIMUM_DISPLAY_APERTURE, |
| reinterpret_cast<UINT8*>(&videoCropArea), |
| sizeof(videoCropArea), nullptr))) { |
| hr = qt_evr_validateVideoArea(videoCropArea, width, height); |
| } |
| return hr; |
| } |
| |
| void EVRCustomPresenter::processOutputLoop() |
| { |
| HRESULT hr = S_OK; |
| |
| // Process as many samples as possible. |
| while (hr == S_OK) { |
| // If the mixer doesn't have a new input sample, break from the loop. |
| if (!m_sampleNotify) { |
| hr = MF_E_TRANSFORM_NEED_MORE_INPUT; |
| break; |
| } |
| |
| // Try to process a sample. |
| hr = processOutput(); |
| |
| // NOTE: ProcessOutput can return S_FALSE to indicate it did not |
| // process a sample. If so, break out of the loop. |
| } |
| |
| if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT) { |
| // The mixer has run out of input data. Check for end-of-stream. |
| checkEndOfStream(); |
| } |
| } |
| |
| HRESULT EVRCustomPresenter::processOutput() |
| { |
| HRESULT hr = S_OK; |
| DWORD status = 0; |
| LONGLONG mixerStartTime = 0, mixerEndTime = 0; |
| MFTIME systemTime = 0; |
| BOOL repaint = m_repaint; // Temporarily store this state flag. |
| |
| MFT_OUTPUT_DATA_BUFFER dataBuffer; |
| ZeroMemory(&dataBuffer, sizeof(dataBuffer)); |
| |
| IMFSample *sample = NULL; |
| |
| // If the clock is not running, we present the first sample, |
| // and then don't present any more until the clock starts. |
| |
| if ((m_renderState != RenderStarted) && !m_repaint && m_prerolled) |
| return S_FALSE; |
| |
| // Make sure we have a pointer to the mixer. |
| if (!m_mixer) |
| return MF_E_INVALIDREQUEST; |
| |
| // Try to get a free sample from the video sample pool. |
| hr = m_samplePool.getSample(&sample); |
| if (hr == MF_E_SAMPLEALLOCATOR_EMPTY) // No free samples. Try again when a sample is released. |
| return S_FALSE; |
| if (FAILED(hr)) |
| return hr; |
| |
| // From now on, we have a valid video sample pointer, where the mixer will |
| // write the video data. |
| |
| if (m_repaint) { |
| // Repaint request. Ask the mixer for the most recent sample. |
| setDesiredSampleTime(sample, m_scheduler.lastSampleTime(), m_scheduler.frameDuration()); |
| |
| m_repaint = false; // OK to clear this flag now. |
| } else { |
| // Not a repaint request. Clear the desired sample time; the mixer will |
| // give us the next frame in the stream. |
| clearDesiredSampleTime(sample); |
| |
| if (m_clock) { |
| // Latency: Record the starting time for ProcessOutput. |
| m_clock->GetCorrelatedTime(0, &mixerStartTime, &systemTime); |
| } |
| } |
| |
| // Now we are ready to get an output sample from the mixer. |
| dataBuffer.dwStreamID = 0; |
| dataBuffer.pSample = sample; |
| dataBuffer.dwStatus = 0; |
| |
| hr = m_mixer->ProcessOutput(0, 1, &dataBuffer, &status); |
| |
| if (FAILED(hr)) { |
| // Return the sample to the pool. |
| HRESULT hr2 = m_samplePool.returnSample(sample); |
| if (FAILED(hr2)) { |
| hr = hr2; |
| goto done; |
| } |
| // Handle some known error codes from ProcessOutput. |
| if (hr == MF_E_TRANSFORM_TYPE_NOT_SET) { |
| // The mixer's format is not set. Negotiate a new format. |
| hr = renegotiateMediaType(); |
| } else if (hr == MF_E_TRANSFORM_STREAM_CHANGE) { |
| // There was a dynamic media type change. Clear our media type. |
| setMediaType(NULL); |
| } else if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT) { |
| // The mixer needs more input. |
| // We have to wait for the mixer to get more input. |
| m_sampleNotify = false; |
| } |
| } else { |
| // We got an output sample from the mixer. |
| |
| if (m_clock && !repaint) { |
| // Latency: Record the ending time for the ProcessOutput operation, |
| // and notify the EVR of the latency. |
| |
| m_clock->GetCorrelatedTime(0, &mixerEndTime, &systemTime); |
| |
| LONGLONG latencyTime = mixerEndTime - mixerStartTime; |
| notifyEvent(EC_PROCESSING_LATENCY, reinterpret_cast<LONG_PTR>(&latencyTime), 0); |
| } |
| |
| // Set up notification for when the sample is released. |
| hr = trackSample(sample); |
| if (FAILED(hr)) |
| goto done; |
| |
| // Schedule the sample. |
| if ((m_frameStep.state == FrameStepNone) || repaint) { |
| hr = deliverSample(sample, repaint); |
| if (FAILED(hr)) |
| goto done; |
| } else { |
| // We are frame-stepping (and this is not a repaint request). |
| hr = deliverFrameStepSample(sample); |
| if (FAILED(hr)) |
| goto done; |
| } |
| |
| m_prerolled = true; // We have presented at least one sample now. |
| } |
| |
| done: |
| qt_evr_safe_release(&sample); |
| |
| // Important: Release any events returned from the ProcessOutput method. |
| qt_evr_safe_release(&dataBuffer.pEvents); |
| return hr; |
| } |
| |
| HRESULT EVRCustomPresenter::deliverSample(IMFSample *sample, bool repaint) |
| { |
| // If we are not actively playing, OR we are scrubbing (rate = 0) OR this is a |
| // repaint request, then we need to present the sample immediately. Otherwise, |
| // schedule it normally. |
| |
| bool presentNow = ((m_renderState != RenderStarted) || isScrubbing() || repaint); |
| |
| HRESULT hr = m_scheduler.scheduleSample(sample, presentNow); |
| |
| if (FAILED(hr)) { |
| // Notify the EVR that we have failed during streaming. The EVR will notify the |
| // pipeline. |
| |
| notifyEvent(EC_ERRORABORT, hr, 0); |
| } |
| |
| return hr; |
| } |
| |
| HRESULT EVRCustomPresenter::deliverFrameStepSample(IMFSample *sample) |
| { |
| HRESULT hr = S_OK; |
| IUnknown *unk = NULL; |
| |
| // For rate 0, discard any sample that ends earlier than the clock time. |
| if (isScrubbing() && m_clock && qt_evr_isSampleTimePassed(m_clock, sample)) { |
| // Discard this sample. |
| } else if (m_frameStep.state >= FrameStepScheduled) { |
| // A frame was already submitted. Put this sample on the frame-step queue, |
| // in case we are asked to step to the next frame. If frame-stepping is |
| // cancelled, this sample will be processed normally. |
| sample->AddRef(); |
| m_frameStep.samples.append(sample); |
| } else { |
| // We're ready to frame-step. |
| |
| // Decrement the number of steps. |
| if (m_frameStep.steps > 0) |
| m_frameStep.steps--; |
| |
| if (m_frameStep.steps > 0) { |
| // This is not the last step. Discard this sample. |
| } else if (m_frameStep.state == FrameStepWaitingStart) { |
| // This is the right frame, but the clock hasn't started yet. Put the |
| // sample on the frame-step queue. When the clock starts, the sample |
| // will be processed. |
| sample->AddRef(); |
| m_frameStep.samples.append(sample); |
| } else { |
| // This is the right frame *and* the clock has started. Deliver this sample. |
| hr = deliverSample(sample, false); |
| if (FAILED(hr)) |
| goto done; |
| |
| // Query for IUnknown so that we can identify the sample later. |
| // Per COM rules, an object always returns the same pointer when QI'ed for IUnknown. |
| hr = sample->QueryInterface(IID_PPV_ARGS(&unk)); |
| if (FAILED(hr)) |
| goto done; |
| |
| m_frameStep.sampleNoRef = reinterpret_cast<DWORD_PTR>(unk); // No add-ref. |
| |
| // NOTE: We do not AddRef the IUnknown pointer, because that would prevent the |
| // sample from invoking the OnSampleFree callback after the sample is presented. |
| // We use this IUnknown pointer purely to identify the sample later; we never |
| // attempt to dereference the pointer. |
| |
| m_frameStep.state = FrameStepScheduled; |
| } |
| } |
| done: |
| qt_evr_safe_release(&unk); |
| return hr; |
| } |
| |
| HRESULT EVRCustomPresenter::trackSample(IMFSample *sample) |
| { |
| IMFTrackedSample *tracked = NULL; |
| |
| HRESULT hr = sample->QueryInterface(IID_PPV_ARGS(&tracked)); |
| |
| if (SUCCEEDED(hr)) |
| hr = tracked->SetAllocator(&m_sampleFreeCB, NULL); |
| |
| qt_evr_safe_release(&tracked); |
| return hr; |
| } |
| |
| void EVRCustomPresenter::releaseResources() |
| { |
| // Increment the token counter to indicate that all existing video samples |
| // are "stale." As these samples get released, we'll dispose of them. |
| // |
| // Note: The token counter is required because the samples are shared |
| // between more than one thread, and they are returned to the presenter |
| // through an asynchronous callback (onSampleFree). Without the token, we |
| // might accidentally re-use a stale sample after the ReleaseResources |
| // method returns. |
| |
| m_tokenCounter++; |
| |
| flush(); |
| |
| m_samplePool.clear(); |
| |
| m_presentEngine->releaseResources(); |
| } |
| |
| HRESULT EVRCustomPresenter::onSampleFree(IMFAsyncResult *result) |
| { |
| IUnknown *object = NULL; |
| IMFSample *sample = NULL; |
| IUnknown *unk = NULL; |
| UINT32 token; |
| |
| // Get the sample from the async result object. |
| HRESULT hr = result->GetObject(&object); |
| if (FAILED(hr)) |
| goto done; |
| |
| hr = object->QueryInterface(IID_PPV_ARGS(&sample)); |
| if (FAILED(hr)) |
| goto done; |
| |
| // If this sample was submitted for a frame-step, the frame step operation |
| // is complete. |
| |
| if (m_frameStep.state == FrameStepScheduled) { |
| // Query the sample for IUnknown and compare it to our cached value. |
| hr = sample->QueryInterface(IID_PPV_ARGS(&unk)); |
| if (FAILED(hr)) |
| goto done; |
| |
| if (m_frameStep.sampleNoRef == reinterpret_cast<DWORD_PTR>(unk)) { |
| // Notify the EVR. |
| hr = completeFrameStep(sample); |
| if (FAILED(hr)) |
| goto done; |
| } |
| |
| // Note: Although object is also an IUnknown pointer, it is not |
| // guaranteed to be the exact pointer value returned through |
| // QueryInterface. Therefore, the second QueryInterface call is |
| // required. |
| } |
| |
| m_mutex.lock(); |
| |
| token = MFGetAttributeUINT32(sample, MFSamplePresenter_SampleCounter, (UINT32)-1); |
| |
| if (token == m_tokenCounter) { |
| // Return the sample to the sample pool. |
| hr = m_samplePool.returnSample(sample); |
| if (SUCCEEDED(hr)) { |
| // A free sample is available. Process more data if possible. |
| processOutputLoop(); |
| } |
| } |
| |
| m_mutex.unlock(); |
| |
| done: |
| if (FAILED(hr)) |
| notifyEvent(EC_ERRORABORT, hr, 0); |
| qt_evr_safe_release(&object); |
| qt_evr_safe_release(&sample); |
| qt_evr_safe_release(&unk); |
| return hr; |
| } |
| |
| float EVRCustomPresenter::getMaxRate(bool thin) |
| { |
| // Non-thinned: |
| // If we have a valid frame rate and a monitor refresh rate, the maximum |
| // playback rate is equal to the refresh rate. Otherwise, the maximum rate |
| // is unbounded (FLT_MAX). |
| |
| // Thinned: The maximum rate is unbounded. |
| |
| float maxRate = FLT_MAX; |
| MFRatio fps = { 0, 0 }; |
| UINT monitorRateHz = 0; |
| |
| if (!thin && m_mediaType) { |
| qt_evr_getFrameRate(m_mediaType, &fps); |
| monitorRateHz = m_presentEngine->refreshRate(); |
| |
| if (fps.Denominator && fps.Numerator && monitorRateHz) { |
| // Max Rate = Refresh Rate / Frame Rate |
| maxRate = (float)MulDiv(monitorRateHz, fps.Denominator, fps.Numerator); |
| } |
| } |
| |
| return maxRate; |
| } |
| |
| bool EVRCustomPresenter::event(QEvent *e) |
| { |
| switch (int(e->type())) { |
| case StartSurface: |
| startSurface(); |
| return true; |
| case StopSurface: |
| stopSurface(); |
| return true; |
| case PresentSample: |
| presentSample(static_cast<PresentSampleEvent *>(e)->sample()); |
| return true; |
| default: |
| break; |
| } |
| return QObject::event(e); |
| } |
| |
| void EVRCustomPresenter::startSurface() |
| { |
| if (thread() != QThread::currentThread()) { |
| QCoreApplication::postEvent(this, new QEvent(QEvent::Type(StartSurface))); |
| return; |
| } |
| |
| if (!m_surface || m_surface->isActive()) |
| return; |
| |
| QVideoSurfaceFormat format = m_presentEngine->videoSurfaceFormat(); |
| if (!format.isValid()) |
| return; |
| |
| m_surface->start(format); |
| } |
| |
| void EVRCustomPresenter::stopSurface() |
| { |
| if (thread() != QThread::currentThread()) { |
| QCoreApplication::postEvent(this, new QEvent(QEvent::Type(StopSurface))); |
| return; |
| } |
| |
| if (!m_surface || !m_surface->isActive()) |
| return; |
| |
| m_surface->stop(); |
| } |
| |
| void EVRCustomPresenter::presentSample(IMFSample *sample) |
| { |
| if (thread() != QThread::currentThread()) { |
| QCoreApplication::postEvent(this, new PresentSampleEvent(sample)); |
| return; |
| } |
| |
| if (!m_surface || !m_presentEngine->videoSurfaceFormat().isValid()) |
| return; |
| |
| QVideoFrame frame = m_presentEngine->makeVideoFrame(sample); |
| |
| // Since start/end times are related to a position when the clock is started, |
| // to have times from the beginning, need to adjust it by adding seeked position. |
| if (m_positionOffset) { |
| if (frame.startTime()) |
| frame.setStartTime(frame.startTime() + m_positionOffset); |
| if (frame.endTime()) |
| frame.setEndTime(frame.endTime() + m_positionOffset); |
| } |
| |
| if (!m_surface->isActive() || m_surface->surfaceFormat() != m_presentEngine->videoSurfaceFormat()) { |
| m_surface->stop(); |
| if (!m_surface->start(m_presentEngine->videoSurfaceFormat())) |
| return; |
| } |
| |
| m_surface->present(frame); |
| } |
| |
| void EVRCustomPresenter::positionChanged(qint64 position) |
| { |
| m_positionOffset = position * 1000; |
| } |
| |
| HRESULT setDesiredSampleTime(IMFSample *sample, const LONGLONG &sampleTime, const LONGLONG &duration) |
| { |
| if (!sample) |
| return E_POINTER; |
| |
| HRESULT hr = S_OK; |
| IMFDesiredSample *desired = NULL; |
| |
| hr = sample->QueryInterface(IID_PPV_ARGS(&desired)); |
| if (SUCCEEDED(hr)) |
| desired->SetDesiredSampleTimeAndDuration(sampleTime, duration); |
| |
| qt_evr_safe_release(&desired); |
| return hr; |
| } |
| |
| HRESULT clearDesiredSampleTime(IMFSample *sample) |
| { |
| if (!sample) |
| return E_POINTER; |
| |
| HRESULT hr = S_OK; |
| |
| IMFDesiredSample *desired = NULL; |
| IUnknown *unkSwapChain = NULL; |
| |
| // We store some custom attributes on the sample, so we need to cache them |
| // and reset them. |
| // |
| // This works around the fact that IMFDesiredSample::Clear() removes all of the |
| // attributes from the sample. |
| |
| UINT32 counter = MFGetAttributeUINT32(sample, MFSamplePresenter_SampleCounter, (UINT32)-1); |
| |
| hr = sample->QueryInterface(IID_PPV_ARGS(&desired)); |
| if (SUCCEEDED(hr)) { |
| desired->Clear(); |
| |
| hr = sample->SetUINT32(MFSamplePresenter_SampleCounter, counter); |
| if (FAILED(hr)) |
| goto done; |
| } |
| |
| done: |
| qt_evr_safe_release(&unkSwapChain); |
| qt_evr_safe_release(&desired); |
| return hr; |
| } |
| |
| HRESULT setMixerSourceRect(IMFTransform *mixer, const MFVideoNormalizedRect &sourceRect) |
| { |
| if (!mixer) |
| return E_POINTER; |
| |
| IMFAttributes *attributes = NULL; |
| |
| HRESULT hr = mixer->GetAttributes(&attributes); |
| if (SUCCEEDED(hr)) { |
| hr = attributes->SetBlob(video_ZOOM_RECT, reinterpret_cast<const UINT8*>(&sourceRect), |
| sizeof(sourceRect)); |
| attributes->Release(); |
| } |
| return hr; |
| } |
| |
| static QVideoFrame::PixelFormat pixelFormatFromMediaType(IMFMediaType *type) |
| { |
| GUID majorType; |
| if (FAILED(type->GetMajorType(&majorType))) |
| return QVideoFrame::Format_Invalid; |
| if (majorType != MFMediaType_Video) |
| return QVideoFrame::Format_Invalid; |
| |
| GUID subtype; |
| if (FAILED(type->GetGUID(MF_MT_SUBTYPE, &subtype))) |
| return QVideoFrame::Format_Invalid; |
| |
| if (subtype == MFVideoFormat_RGB32) |
| return QVideoFrame::Format_RGB32; |
| if (subtype == MFVideoFormat_ARGB32) |
| return QVideoFrame::Format_ARGB32; |
| if (subtype == MFVideoFormat_RGB24) |
| return QVideoFrame::Format_RGB24; |
| if (subtype == MFVideoFormat_RGB565) |
| return QVideoFrame::Format_RGB565; |
| if (subtype == MFVideoFormat_RGB555) |
| return QVideoFrame::Format_RGB555; |
| if (subtype == MFVideoFormat_AYUV) |
| return QVideoFrame::Format_AYUV444; |
| if (subtype == MFVideoFormat_I420) |
| return QVideoFrame::Format_YUV420P; |
| if (subtype == MFVideoFormat_UYVY) |
| return QVideoFrame::Format_UYVY; |
| if (subtype == MFVideoFormat_YV12) |
| return QVideoFrame::Format_YV12; |
| if (subtype == MFVideoFormat_NV12) |
| return QVideoFrame::Format_NV12; |
| |
| return QVideoFrame::Format_Invalid; |
| } |
| |
| QT_END_NAMESPACE |