/*
 * Copyright (C) 2014 Alex Christensen <achristensen@webkit.org>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "MediaPlayerPrivateMediaFoundation.h"

#include "CachedResourceLoader.h"
#include "FrameView.h"
#include "GraphicsContext.h"
#include "HWndDC.h"
#include "HostWindow.h"
#include "NotImplemented.h"
#if USE(CAIRO)
#include "CairoOperations.h"
#include "PlatformContextCairo.h"
#include <cairo.h>
#endif

#if USE(MEDIA_FOUNDATION)

#include <wtf/MainThread.h>
#include <wtf/NeverDestroyed.h>

// MFSamplePresenterSampleCounter
// Data type: UINT32
//
// Version number for the video samples. When the presenter increments the version
// number, all samples with the previous version number are stale and should be
// discarded.
static const GUID MFSamplePresenterSampleCounter =
{ 0x869f1f7c, 0x3496, 0x48a9, { 0x88, 0xe3, 0x69, 0x85, 0x79, 0xd0, 0x8c, 0xb6 } };

static const double tenMegahertz = 10000000;

namespace WebCore {

MediaPlayerPrivateMediaFoundation::MediaPlayerPrivateMediaFoundation(MediaPlayer* player) 
    : m_weakThis(makeWeakPtr(this))
    , m_player(player)
    , m_visible(false)
    , m_loadingProgress(false)
    , m_paused(true)
    , m_hasAudio(false)
    , m_hasVideo(false)
    , m_preparingToPlay(false)
    , m_volume(1.0)
    , m_networkState(MediaPlayer::Empty)
    , m_readyState(MediaPlayer::HaveNothing)
{
    createSession();
}

MediaPlayerPrivateMediaFoundation::~MediaPlayerPrivateMediaFoundation()
{
    notifyDeleted();
    endSession();
}

void MediaPlayerPrivateMediaFoundation::registerMediaEngine(MediaEngineRegistrar registrar)
{
    if (isAvailable()) {
        registrar([](MediaPlayer* player) { return std::make_unique<MediaPlayerPrivateMediaFoundation>(player); },
            getSupportedTypes, supportsType, 0, 0, 0, 0);
    }
}

bool MediaPlayerPrivateMediaFoundation::isAvailable() 
{
    notImplemented();
    return true;
}

static const HashSet<String, ASCIICaseInsensitiveHash>& mimeTypeCache()
{
    static NeverDestroyed<HashSet<String, ASCIICaseInsensitiveHash>> cachedTypes;

    if (cachedTypes.get().size() > 0)
        return cachedTypes;

    cachedTypes.get().add(String("video/mp4"));

    PROPVARIANT propVarMimeTypeArray;
    PropVariantInit(&propVarMimeTypeArray);

    HRESULT hr = MFGetSupportedMimeTypes(&propVarMimeTypeArray);

    if (SUCCEEDED(hr)) {
        CALPWSTR mimeTypeArray = propVarMimeTypeArray.calpwstr;
        for (unsigned i = 0; i < mimeTypeArray.cElems; i++)
            cachedTypes.get().add(mimeTypeArray.pElems[i]);
    }

    PropVariantClear(&propVarMimeTypeArray);

    return cachedTypes;
}

void MediaPlayerPrivateMediaFoundation::getSupportedTypes(HashSet<String, ASCIICaseInsensitiveHash>& types)
{
    types = mimeTypeCache();
}

MediaPlayer::SupportsType MediaPlayerPrivateMediaFoundation::supportsType(const MediaEngineSupportParameters& parameters)
{
    if (parameters.type.isEmpty())
        return MediaPlayer::IsNotSupported;

    if (mimeTypeCache().contains(parameters.type.containerType()))
        return MediaPlayer::IsSupported;

    return MediaPlayer::IsNotSupported;
}

void MediaPlayerPrivateMediaFoundation::load(const String& url)
{
    {
        LockHolder locker(m_cachedNaturalSizeLock);
        m_cachedNaturalSize = FloatSize();
    }

    startCreateMediaSource(url);

    m_networkState = MediaPlayer::Loading;
    m_player->networkStateChanged();
    m_readyState = MediaPlayer::HaveNothing;
    m_player->readyStateChanged();
}

void MediaPlayerPrivateMediaFoundation::cancelLoad()
{
    notImplemented();
}

void MediaPlayerPrivateMediaFoundation::play()
{
    m_paused = !startSession();

    m_preparingToPlay = false;
}

void MediaPlayerPrivateMediaFoundation::pause()
{
    if (!m_mediaSession)
        return;

    m_paused = SUCCEEDED(m_mediaSession->Pause());
}

bool MediaPlayerPrivateMediaFoundation::supportsFullscreen() const
{
    return true;
}

FloatSize MediaPlayerPrivateMediaFoundation::naturalSize() const
{
    LockHolder locker(m_cachedNaturalSizeLock);
    return m_cachedNaturalSize;
}

bool MediaPlayerPrivateMediaFoundation::hasVideo() const
{
    return m_hasVideo;
}

bool MediaPlayerPrivateMediaFoundation::hasAudio() const
{
    return m_hasAudio;
}

void MediaPlayerPrivateMediaFoundation::setVisible(bool visible)
{
    m_visible = visible;
}

bool MediaPlayerPrivateMediaFoundation::seeking() const
{
    // We assume seeking is immediately complete.
    return false;
}

void MediaPlayerPrivateMediaFoundation::seek(float time)
{
    PROPVARIANT propVariant;
    PropVariantInit(&propVariant);
    propVariant.vt = VT_I8;
    propVariant.hVal.QuadPart = static_cast<__int64>(time * tenMegahertz);
    
    HRESULT hr = m_mediaSession->Start(&GUID_NULL, &propVariant);
    ASSERT_UNUSED(hr, SUCCEEDED(hr));
    PropVariantClear(&propVariant);

    m_player->timeChanged();
}

void MediaPlayerPrivateMediaFoundation::setRate(float rate)
{
    COMPtr<IMFRateControl> rateControl;

    HRESULT hr = MFGetService(m_mediaSession.get(), MF_RATE_CONTROL_SERVICE, IID_IMFRateControl, (void**)&rateControl);

    if (!SUCCEEDED(hr))
        return;

    BOOL reduceSamplesInStream = rate > 2.0;

    rateControl->SetRate(reduceSamplesInStream, rate);
}

float MediaPlayerPrivateMediaFoundation::duration() const
{
    if (!m_mediaSource)
        return 0;

    IMFPresentationDescriptor* descriptor;
    if (!SUCCEEDED(m_mediaSource->CreatePresentationDescriptor(&descriptor)))
        return 0;
    
    UINT64 duration;
    if (!SUCCEEDED(descriptor->GetUINT64(MF_PD_DURATION, &duration)))
        duration = 0;
    descriptor->Release();
    
    return static_cast<float>(duration) / tenMegahertz;
}

float MediaPlayerPrivateMediaFoundation::currentTime() const
{
    if (!m_presenter)
        return 0.0f;

    return m_presenter->currentTime();
}

bool MediaPlayerPrivateMediaFoundation::paused() const
{
    return m_paused;
}

bool MediaPlayerPrivateMediaFoundation::setAllChannelVolumes(float volume)
{
    COMPtr<IMFAudioStreamVolume> audioVolume;
    if (!SUCCEEDED(MFGetService(m_mediaSession.get(), MR_STREAM_VOLUME_SERVICE, __uuidof(IMFAudioStreamVolume), (void **)&audioVolume)))
        return false;

    UINT32 channelsCount;
    HRESULT hr = audioVolume->GetChannelCount(&channelsCount);
    ASSERT_UNUSED(hr, SUCCEEDED(hr));

    Vector<float> volumes(channelsCount, volume);
    return SUCCEEDED(audioVolume->SetAllVolumes(channelsCount, volumes.data()));
}

void MediaPlayerPrivateMediaFoundation::setVolume(float volume)
{
    if (setAllChannelVolumes(volume))
        m_volume = volume;
}

bool MediaPlayerPrivateMediaFoundation::supportsMuting() const
{
    return true;
}

void MediaPlayerPrivateMediaFoundation::setMuted(bool muted)
{
    setAllChannelVolumes(muted ? 0.0 : m_volume);
}

MediaPlayer::NetworkState MediaPlayerPrivateMediaFoundation::networkState() const
{ 
    return m_networkState;
}

MediaPlayer::ReadyState MediaPlayerPrivateMediaFoundation::readyState() const
{
    return m_readyState;
}

float MediaPlayerPrivateMediaFoundation::maxTimeSeekable() const
{
    return durationDouble();
}

std::unique_ptr<PlatformTimeRanges> MediaPlayerPrivateMediaFoundation::buffered() const
{ 
    auto ranges = std::make_unique<PlatformTimeRanges>();
    if (m_presenter && m_presenter->maxTimeLoaded() > 0)
        ranges->add(MediaTime::zeroTime(), MediaTime::createWithDouble(m_presenter->maxTimeLoaded()));
    return ranges;
}

bool MediaPlayerPrivateMediaFoundation::didLoadingProgress() const
{
    return m_loadingProgress;
}

void MediaPlayerPrivateMediaFoundation::setSize(const IntSize& size)
{
    m_size = size;

    auto videoDisplay = this->videoDisplay();
    if (!videoDisplay)
        return;

    FrameView* view = nullptr;
    float deviceScaleFactor = 1.0f;
    if (m_player && m_player->cachedResourceLoader() && m_player->cachedResourceLoader()->document()) {
        view = m_player->cachedResourceLoader()->document()->view();
        deviceScaleFactor = m_player->cachedResourceLoader()->document()->deviceScaleFactor();
    }

    int w = m_size.width() * deviceScaleFactor;
    int h = m_size.height() * deviceScaleFactor;

    RECT rc = { 0, 0, w, h };
    videoDisplay->SetVideoPosition(nullptr, &rc);
}

void MediaPlayerPrivateMediaFoundation::paint(GraphicsContext& context, const FloatRect& rect)
{
    if (context.paintingDisabled() || !m_player->visible())
        return;

    if (m_presenter)
        m_presenter->paintCurrentFrame(context, rect);
}

bool MediaPlayerPrivateMediaFoundation::createSession()
{
    if (FAILED(MFStartup(MF_VERSION, MFSTARTUP_FULL)))
        return false;

    if (FAILED(MFCreateMediaSession(nullptr, &m_mediaSession)))
        return false;

    // Get next event.
    AsyncCallback* callback = new AsyncCallback(this, true);
    HRESULT hr = m_mediaSession->BeginGetEvent(callback, nullptr);
    ASSERT_UNUSED(hr, SUCCEEDED(hr));

    return true;
}

bool MediaPlayerPrivateMediaFoundation::startSession()
{
    if (!m_mediaSession)
        return false;

    PROPVARIANT varStart;
    PropVariantInit(&varStart);
    varStart.vt = VT_EMPTY;

    HRESULT hr = m_mediaSession->Start(nullptr, &varStart);
    ASSERT(SUCCEEDED(hr));

    PropVariantClear(&varStart);

    return SUCCEEDED(hr);
}

bool MediaPlayerPrivateMediaFoundation::endSession()
{
    if (m_mediaSession) {
        m_mediaSession->Shutdown();
        m_mediaSession = nullptr;
    }

    HRESULT hr = MFShutdown();
    ASSERT_UNUSED(hr, SUCCEEDED(hr));

    return true;
}

bool MediaPlayerPrivateMediaFoundation::startCreateMediaSource(const String& url)
{
    if (FAILED(MFCreateSourceResolver(&m_sourceResolver)))
        return false;

    COMPtr<IUnknown> cancelCookie;
    Vector<wchar_t> urlSource = url.wideCharacters();

    AsyncCallback* callback = new AsyncCallback(this, false);

    if (FAILED(m_sourceResolver->BeginCreateObjectFromURL(urlSource.data(), MF_RESOLUTION_MEDIASOURCE, nullptr, &cancelCookie, callback, nullptr)))
        return false;

    return true;
}

bool MediaPlayerPrivateMediaFoundation::endCreatedMediaSource(IMFAsyncResult* asyncResult)
{
    MF_OBJECT_TYPE objectType;
    COMPtr<IUnknown> source;

    HRESULT hr = m_sourceResolver->EndCreateObjectFromURL(asyncResult, &objectType, &source);
    if (FAILED(hr))
        return false;

    hr = source->QueryInterface(IID_PPV_ARGS(&m_mediaSource));
    if (FAILED(hr))
        return false;

    hr = asyncResult->GetStatus();
    m_loadingProgress = SUCCEEDED(hr);

    callOnMainThread([weakPtr = m_weakThis] {
        if (!weakPtr)
            return;
        weakPtr->onCreatedMediaSource();
    });

    return true;
}

bool MediaPlayerPrivateMediaFoundation::endGetEvent(IMFAsyncResult* asyncResult)
{
    COMPtr<IMFMediaEvent> event;

    if (!m_mediaSession)
        return false;

    // Get the event from the event queue.
    HRESULT hr = m_mediaSession->EndGetEvent(asyncResult, &event);
    if (FAILED(hr))
        return false;

    // Get the event type.
    MediaEventType mediaEventType;
    hr = event->GetType(&mediaEventType);
    if (FAILED(hr))
        return false;

    switch (mediaEventType) {
    case MESessionTopologySet: {
        callOnMainThread([weakPtr = m_weakThis] {
            if (!weakPtr)
                return;
            weakPtr->onTopologySet();
        });
        break;
    }

    case MESessionStarted: {
        callOnMainThread([weakPtr = m_weakThis] {
            if (!weakPtr)
                return;
            weakPtr->onSessionStarted();
        });
        break;
    }

    case MEBufferingStarted: {
        callOnMainThread([weakPtr = m_weakThis] {
            if (!weakPtr)
                return;
            weakPtr->onBufferingStarted();
        });
        break;
    }

    case MEBufferingStopped: {
        callOnMainThread([weakPtr = m_weakThis] {
            if (!weakPtr)
                return;
            weakPtr->onBufferingStopped();
        });
        break;
    }

    case MESessionEnded: {
        callOnMainThread([weakPtr = m_weakThis] {
            if (!weakPtr)
                return;
            weakPtr->onSessionEnded();
        });
        break;
    }

    case MEMediaSample:
        break;

    case MEError: {
        HRESULT status = S_OK;
        event->GetStatus(&status);
        break;
    }
    }

    if (mediaEventType != MESessionClosed) {
        // For all other events, ask the media session for the
        // next event in the queue.
        AsyncCallback* callback = new AsyncCallback(this, true);

        hr = m_mediaSession->BeginGetEvent(callback, nullptr);
        if (FAILED(hr))
            return false;
    }

    return true;
}

bool MediaPlayerPrivateMediaFoundation::createTopologyFromSource()
{
    // Create a new topology.
    if (FAILED(MFCreateTopology(&m_topology)))
        return false;

    // Create the presentation descriptor for the media source.
    if (FAILED(m_mediaSource->CreatePresentationDescriptor(&m_sourcePD)))
        return false;

    // Get the number of streams in the media source.
    DWORD sourceStreams = 0;
    if (FAILED(m_sourcePD->GetStreamDescriptorCount(&sourceStreams)))
        return false;

    // For each stream, create the topology nodes and add them to the topology.
    for (DWORD i = 0; i < sourceStreams; i++) {
        if (!addBranchToPartialTopology(i))
            return false;
    }

    return true;
}

bool MediaPlayerPrivateMediaFoundation::addBranchToPartialTopology(int stream)
{
    // Get the stream descriptor for this stream.
    COMPtr<IMFStreamDescriptor> sourceSD;
    BOOL selected = FALSE;
    if (FAILED(m_sourcePD->GetStreamDescriptorByIndex(stream, &selected, &sourceSD)))
        return false;

    // Create the topology branch only if the stream is selected.
    // Otherwise, do nothing.
    if (!selected)
        return true;

    // Create a source node for this stream.
    COMPtr<IMFTopologyNode> sourceNode;
    if (!createSourceStreamNode(sourceSD, sourceNode))
        return false;

    COMPtr<IMFTopologyNode> outputNode;
    if (!createOutputNode(sourceSD, outputNode))
        return false;

    // Add both nodes to the topology.
    if (FAILED(m_topology->AddNode(sourceNode.get())))
        return false;

    if (FAILED(m_topology->AddNode(outputNode.get())))
        return false;

    // Connect the source node to the output node.
    if (FAILED(sourceNode->ConnectOutput(0, outputNode.get(), 0)))
        return false;

    return true;
}

HWND MediaPlayerPrivateMediaFoundation::hostWindow()
{
    if (m_player && m_player->cachedResourceLoader() && !m_player->cachedResourceLoader()->document()) {
        auto* view = m_player->cachedResourceLoader()->document()->view();
        if (view && view->hostWindow() && view->hostWindow()->platformPageClient())
            return view->hostWindow()->platformPageClient();
    }
    return GetDesktopWindow();
}

void MediaPlayerPrivateMediaFoundation::invalidateFrameView()
{
    FrameView* view = nullptr;
    if (!m_player || !m_player->cachedResourceLoader() || !m_player->cachedResourceLoader()->document())
        return;
    view = m_player->cachedResourceLoader()->document()->view();
    if (!view)
        return;

    view->invalidate();
}

void MediaPlayerPrivateMediaFoundation::addListener(MediaPlayerListener* listener)
{
    LockHolder locker(m_mutexListeners);

    m_listeners.add(listener);
}

void MediaPlayerPrivateMediaFoundation::removeListener(MediaPlayerListener* listener)
{
    LockHolder locker(m_mutexListeners);

    m_listeners.remove(listener);
}

void MediaPlayerPrivateMediaFoundation::notifyDeleted()
{
    LockHolder locker(m_mutexListeners);

    for (HashSet<MediaPlayerListener*>::const_iterator it = m_listeners.begin(); it != m_listeners.end(); ++it)
        (*it)->onMediaPlayerDeleted();
}

void MediaPlayerPrivateMediaFoundation::setNaturalSize(const FloatSize& size)
{
    LockHolder locker(m_cachedNaturalSizeLock);
    m_cachedNaturalSize = size;
}

bool MediaPlayerPrivateMediaFoundation::createOutputNode(COMPtr<IMFStreamDescriptor> sourceSD, COMPtr<IMFTopologyNode>& node)
{
    if (!sourceSD)
        return false;

#ifndef NDEBUG
    // Get the stream ID.
    DWORD streamID = 0;
    sourceSD->GetStreamIdentifier(&streamID); // Just for debugging, ignore any failures.
#endif

    COMPtr<IMFMediaTypeHandler> handler;
    if (FAILED(sourceSD->GetMediaTypeHandler(&handler)))
        return false;

    GUID guidMajorType = GUID_NULL;
    if (FAILED(handler->GetMajorType(&guidMajorType)))
        return false;

    // Create a downstream node.
    if (FAILED(MFCreateTopologyNode(MF_TOPOLOGY_OUTPUT_NODE, &node)))
        return false;

    // Create an IMFActivate object for the renderer, based on the media type.
    COMPtr<IMFActivate> rendererActivate;
    if (MFMediaType_Audio == guidMajorType) {
        // Create the audio renderer.
        if (FAILED(MFCreateAudioRendererActivate(&rendererActivate)))
            return false;
        m_hasAudio = true;
    } else if (MFMediaType_Video == guidMajorType) {
        // Create the video renderer.
        if (FAILED(MFCreateVideoRendererActivate(nullptr, &rendererActivate)))
            return false;

        m_presenter = new CustomVideoPresenter(this);
        m_presenter->SetVideoWindow(hostWindow());
        if (FAILED(rendererActivate->SetUnknown(MF_ACTIVATE_CUSTOM_VIDEO_PRESENTER_ACTIVATE, static_cast<IMFActivate*>(m_presenter.get()))))
            return false;
        m_hasVideo = true;
    } else
        return false;

    // Set the IActivate object on the output node.
    if (FAILED(node->SetObject(rendererActivate.get())))
        return false;

    return true;
}

bool MediaPlayerPrivateMediaFoundation::createSourceStreamNode(COMPtr<IMFStreamDescriptor> sourceSD, COMPtr<IMFTopologyNode>& node)
{
    if (!m_mediaSource || !m_sourcePD || !sourceSD)
        return false;

    // Create the source-stream node.
    HRESULT hr = MFCreateTopologyNode(MF_TOPOLOGY_SOURCESTREAM_NODE, &node);
    if (FAILED(hr))
        return false;

    // Set attribute: Pointer to the media source.
    hr = node->SetUnknown(MF_TOPONODE_SOURCE, m_mediaSource.get());
    if (FAILED(hr))
        return false;

    // Set attribute: Pointer to the presentation descriptor.
    hr = node->SetUnknown(MF_TOPONODE_PRESENTATION_DESCRIPTOR, m_sourcePD.get());
    if (FAILED(hr))
        return false;

    // Set attribute: Pointer to the stream descriptor.
    hr = node->SetUnknown(MF_TOPONODE_STREAM_DESCRIPTOR, sourceSD.get());
    if (FAILED(hr))
        return false;

    return true;
}

void MediaPlayerPrivateMediaFoundation::updateReadyState()
{
    COMPtr<IPropertyStore> prop;

    // Get the property store from the media session.
    HRESULT hr = MFGetService(m_mediaSession.get(), MFNETSOURCE_STATISTICS_SERVICE, IID_PPV_ARGS(&prop));

    if (FAILED(hr))
        return;

    PROPERTYKEY key;
    key.fmtid = MFNETSOURCE_STATISTICS;
    key.pid = MFNETSOURCE_BUFFERPROGRESS_ID;

    PROPVARIANT var;
    hr = prop->GetValue(key, &var);

    const LONG percentageOfPlaybackBufferFilled = var.lVal;

    PropVariantClear(&var);

    if (FAILED(hr))
        return;

    MediaPlayer::ReadyState oldReadyState = m_readyState;
    if (percentageOfPlaybackBufferFilled >= 100) {
        m_readyState = MediaPlayer::HaveEnoughData;
        if (m_preparingToPlay) {
            pause();
            m_preparingToPlay = false;
        }
    } else if (percentageOfPlaybackBufferFilled > 0)
        m_readyState = MediaPlayer::HaveFutureData;
    else
        m_readyState = MediaPlayer::HaveCurrentData;

    if (m_readyState != oldReadyState)
        m_player->readyStateChanged();
}

COMPtr<IMFVideoDisplayControl> MediaPlayerPrivateMediaFoundation::videoDisplay()
{
    if (m_videoDisplay)
        return m_videoDisplay;

    MFGetService(m_mediaSession.get(), MR_VIDEO_RENDER_SERVICE, IID_PPV_ARGS(&m_videoDisplay));

    return m_videoDisplay;
}

void MediaPlayerPrivateMediaFoundation::onCreatedMediaSource()
{
    if (!createTopologyFromSource())
        return;

    // Set the topology on the media session.
    HRESULT hr = m_mediaSession->SetTopology(0, m_topology.get());
    ASSERT_UNUSED(hr, SUCCEEDED(hr));
}

void MediaPlayerPrivateMediaFoundation::onTopologySet()
{
    // This method is called on the main thread as a result of load() being called.

    if (auto videoDisplay = this->videoDisplay()) {
        RECT rc = { 0, 0, m_size.width(), m_size.height() };
        videoDisplay->SetVideoPosition(nullptr, &rc);
    }

    // It is expected that we start buffering data from the network now.
    // We call startSession() to start buffering video data.
    // When we have received enough data, we pause, so that we don't actually start the playback.
    ASSERT(m_paused);
    ASSERT(!m_preparingToPlay);
    m_preparingToPlay = startSession();
}

void MediaPlayerPrivateMediaFoundation::onBufferingStarted()
{
    updateReadyState();
}

void MediaPlayerPrivateMediaFoundation::onBufferingStopped()
{
    updateReadyState();
}

void MediaPlayerPrivateMediaFoundation::onSessionStarted()
{
    updateReadyState();
}

void MediaPlayerPrivateMediaFoundation::onSessionEnded()
{
    m_networkState = MediaPlayer::Loaded;
    m_player->networkStateChanged();

    m_paused = true;
    m_player->playbackStateChanged();
}

MediaPlayerPrivateMediaFoundation::AsyncCallback::AsyncCallback(MediaPlayerPrivateMediaFoundation* mediaPlayer, bool event)
    : m_refCount(0)
    , m_mediaPlayer(mediaPlayer)
    , m_event(event)
{
    if (m_mediaPlayer)
        m_mediaPlayer->addListener(this);
}

MediaPlayerPrivateMediaFoundation::AsyncCallback::~AsyncCallback()
{
    if (m_mediaPlayer)
        m_mediaPlayer->removeListener(this);
}

HRESULT MediaPlayerPrivateMediaFoundation::AsyncCallback::QueryInterface(_In_ REFIID riid, __RPC__deref_out void __RPC_FAR *__RPC_FAR *ppvObject)
{
    if (!ppvObject)
        return E_POINTER;
    if (!IsEqualGUID(riid, IID_IMFAsyncCallback)) {
        *ppvObject = nullptr;
        return E_NOINTERFACE;
    }
    *ppvObject = this;
    AddRef();
    return S_OK;
}

ULONG STDMETHODCALLTYPE MediaPlayerPrivateMediaFoundation::AsyncCallback::AddRef()
{
    m_refCount++;
    return m_refCount;
}

ULONG STDMETHODCALLTYPE MediaPlayerPrivateMediaFoundation::AsyncCallback::Release()
{
    m_refCount--;
    ULONG refCount = m_refCount;
    if (!refCount)
        delete this;
    return refCount;
}

HRESULT STDMETHODCALLTYPE MediaPlayerPrivateMediaFoundation::AsyncCallback::GetParameters(__RPC__out DWORD *pdwFlags, __RPC__out DWORD *pdwQueue)
{
    // Returning E_NOTIMPL gives default values.
    return E_NOTIMPL;
}

HRESULT STDMETHODCALLTYPE MediaPlayerPrivateMediaFoundation::AsyncCallback::Invoke(__RPC__in_opt IMFAsyncResult *pAsyncResult)
{
    LockHolder locker(m_mutex);

    if (!m_mediaPlayer)
        return S_OK;

    if (m_event)
        m_mediaPlayer->endGetEvent(pAsyncResult);
    else
        m_mediaPlayer->endCreatedMediaSource(pAsyncResult);

    return S_OK;
}

void MediaPlayerPrivateMediaFoundation::AsyncCallback::onMediaPlayerDeleted()
{
    LockHolder locker(m_mutex);

    m_mediaPlayer = nullptr;
}

MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::CustomVideoPresenter(MediaPlayerPrivateMediaFoundation* mediaPlayer)
    : m_mediaPlayer(mediaPlayer)
{
    if (m_mediaPlayer)
        m_mediaPlayer->addListener(this);

    m_sourceRect.top = 0;
    m_sourceRect.left = 0;
    m_sourceRect.bottom = 1;
    m_sourceRect.right = 1;

    m_presenterEngine = std::make_unique<Direct3DPresenter>();
    if (!m_presenterEngine)
        return;

    m_scheduler.setPresenter(m_presenterEngine.get());
}

MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::~CustomVideoPresenter()
{
    if (m_mediaPlayer)
        m_mediaPlayer->removeListener(this);
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::QueryInterface(REFIID riid, __RPC__deref_out void __RPC_FAR *__RPC_FAR *ppvObject)
{
    *ppvObject = nullptr;
    if (IsEqualGUID(riid, IID_IMFGetService))
        *ppvObject = static_cast<IMFGetService*>(this);
    else if (IsEqualGUID(riid, IID_IMFActivate))
        *ppvObject = static_cast<IMFActivate*>(this);
    else if (IsEqualGUID(riid, IID_IMFVideoDisplayControl))
        *ppvObject = static_cast<IMFVideoDisplayControl*>(this);
    else if (IsEqualGUID(riid, IID_IMFVideoPresenter))
        *ppvObject = static_cast<IMFVideoPresenter*>(this);
    else if (IsEqualGUID(riid, IID_IMFClockStateSink))
        *ppvObject = static_cast<IMFClockStateSink*>(this);
    else if (IsEqualGUID(riid, IID_IMFVideoDeviceID))
        *ppvObject = static_cast<IMFVideoDeviceID*>(this);
    else if (IsEqualGUID(riid, IID_IMFTopologyServiceLookupClient))
        *ppvObject = static_cast<IMFTopologyServiceLookupClient*>(this);
    else if (IsEqualGUID(riid, IID_IUnknown))
        *ppvObject = static_cast<IMFVideoPresenter*>(this);
    else if (IsEqualGUID(riid, IID_IMFAsyncCallback))
        *ppvObject = static_cast<IMFAsyncCallback*>(this);
    else
        return E_NOINTERFACE;

    AddRef();
    return S_OK;
}

ULONG MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::AddRef()
{
    m_refCount++;
    return m_refCount;
}

ULONG MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::Release()
{
    m_refCount--;
    ULONG refCount = m_refCount;
    if (!refCount)
        delete this;
    return refCount;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::OnClockStart(MFTIME hnsSystemTime, LONGLONG llClockStartOffset)
{
    LockHolder locker(m_lock);

    // After shutdown, we cannot start.
    HRESULT hr = checkShutdown();
    if (FAILED(hr))
        return hr;

    m_renderState = RenderStateStarted;

    if (isActive()) {
        if (llClockStartOffset != PRESENTATION_CURRENT_POSITION) {
            // This is a seek request, flush pending samples.
            flush();
        }
    }

    processOutputLoop();

    return S_OK;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::OnClockStop(MFTIME hnsSystemTime)
{
    LockHolder locker(m_lock);

    HRESULT hr = checkShutdown();
    if (FAILED(hr))
        return hr;

    if (m_renderState != RenderStateStopped) {
        m_renderState = RenderStateStopped;
        flush();
    }

    return S_OK;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::OnClockPause(MFTIME hnsSystemTime)
{
    LockHolder locker(m_lock);

    // After shutdown, we cannot pause.
    HRESULT hr = checkShutdown();
    if (FAILED(hr))
        return hr;

    m_renderState = RenderStatePaused;

    return S_OK;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::OnClockRestart(MFTIME hnsSystemTime)
{
    LockHolder locker(m_lock);

    HRESULT hr = checkShutdown();
    if (FAILED(hr))
        return hr;

    ASSERT(m_renderState == RenderStatePaused);

    m_renderState = RenderStateStarted;

    processOutputLoop();

    return S_OK;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::OnClockSetRate(MFTIME hnsSystemTime, float rate)
{
    LockHolder locker(m_lock);

    HRESULT hr = checkShutdown();
    if (FAILED(hr))
        return hr;

    m_rate = rate;

    m_scheduler.setClockRate(rate);

    return S_OK;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::ProcessMessage(MFVP_MESSAGE_TYPE eMessage, ULONG_PTR ulParam)
{
    LockHolder locker(m_lock);

    HRESULT hr = checkShutdown();
    if (FAILED(hr))
        return hr;

    switch (eMessage) {
    case MFVP_MESSAGE_FLUSH:
        hr = flush();
        break;

    case MFVP_MESSAGE_INVALIDATEMEDIATYPE:
        hr = renegotiateMediaType();
        break;

    case MFVP_MESSAGE_PROCESSINPUTNOTIFY:
        // A new input sample is available. 
        hr = processInputNotify();
        break;

    case MFVP_MESSAGE_BEGINSTREAMING:
        hr = beginStreaming();
        break;

    case MFVP_MESSAGE_ENDSTREAMING:
        hr = endStreaming();
        break;

    case MFVP_MESSAGE_ENDOFSTREAM:
        m_endStreaming = true;
        hr = checkEndOfStream();
        break;

    default:
        hr = E_INVALIDARG;
        break;
    }

    return hr;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::GetCurrentMediaType(_Outptr_  IMFVideoMediaType **ppMediaType)
{
    LockHolder locker(m_lock);

    if (!ppMediaType)
        return E_POINTER;

    HRESULT hr = checkShutdown();
    if (FAILED(hr))
        return hr;

    if (!m_mediaType)
        return MF_E_NOT_INITIALIZED;

    return m_mediaType->QueryInterface(__uuidof(IMFVideoMediaType), (void**)&ppMediaType);
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::GetDeviceID(IID* pDeviceID)
{
    if (!pDeviceID)
        return E_POINTER;

    *pDeviceID = __uuidof(IDirect3DDevice9);
    return S_OK;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::InitServicePointers(IMFTopologyServiceLookup *pLookup)
{
    if (!pLookup)
        return E_POINTER;

    HRESULT hr = S_OK;

    LockHolder locker(m_lock);

    if (isActive())
        return MF_E_INVALIDREQUEST;

    m_clock = nullptr;
    m_mixer = nullptr;
    m_mediaEventSink = nullptr;

    // Lookup the services.

    DWORD objectCount = 1;
    hr = pLookup->LookupService(MF_SERVICE_LOOKUP_GLOBAL, 0, MR_VIDEO_RENDER_SERVICE, IID_PPV_ARGS(&m_clock), &objectCount);
    // The clock service is optional.

    objectCount = 1;
    hr = pLookup->LookupService(MF_SERVICE_LOOKUP_GLOBAL, 0, MR_VIDEO_MIXER_SERVICE, IID_PPV_ARGS(&m_mixer), &objectCount);
    if (FAILED(hr))
        return hr;

    hr = configureMixer(m_mixer.get());
    if (FAILED(hr))
        return hr;

    objectCount = 1;
    hr = pLookup->LookupService(MF_SERVICE_LOOKUP_GLOBAL, 0, MR_VIDEO_RENDER_SERVICE, IID_PPV_ARGS(&m_mediaEventSink), &objectCount);
    if (FAILED(hr))
        return hr;

    m_renderState = RenderStateStopped;

    return S_OK;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::ReleaseServicePointers()
{
    LockHolder locker(m_lock);

    m_renderState = RenderStateShutdown;

    flush();

    setMediaType(nullptr);

    m_clock = nullptr;
    m_mixer = nullptr;
    m_mediaEventSink = nullptr;

    return S_OK;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::GetService(REFGUID guidService, REFIID riid, LPVOID* ppvObject)
{
    if (!ppvObject)
        return E_POINTER;

    // We only support MR_VIDEO_RENDER_SERVICE.
    if (guidService != MR_VIDEO_RENDER_SERVICE)
        return MF_E_UNSUPPORTED_SERVICE;

    HRESULT hr = m_presenterEngine->getService(guidService, riid, ppvObject);

    if (FAILED(hr))
        hr = QueryInterface(riid, ppvObject);

    return hr;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::ActivateObject(REFIID riid, void **ppv)
{
    if (!ppv)
        return E_POINTER;

    if (riid == IID_IMFVideoPresenter) {
        *ppv = static_cast<IMFVideoPresenter*>(this);
        AddRef();
        return S_OK;
    }
    return E_FAIL;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::DetachObject()
{
    return S_OK;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::ShutdownObject()
{
    return S_OK;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::SetVideoWindow(HWND hwndVideo)
{
    LockHolder locker(m_lock);

    if (!IsWindow(hwndVideo))
        return E_INVALIDARG;

    HRESULT hr = S_OK;
    HWND oldHwnd = m_presenterEngine->getVideoWindow();

    if (oldHwnd != hwndVideo) {
        // This will create a new Direct3D device.
        hr = m_presenterEngine->setVideoWindow(hwndVideo);

        notifyEvent(EC_DISPLAY_CHANGED, 0, 0);
    }

    return hr;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::GetVideoWindow(HWND* phwndVideo)
{
    LockHolder locker(m_lock);

    if (!phwndVideo)
        return E_POINTER;

    *phwndVideo = m_presenterEngine->getVideoWindow();

    return S_OK;
}

static HRESULT setMixerSourceRect(IMFTransform* mixer, const MFVideoNormalizedRect& sourceRect)
{
    if (!mixer)
        return E_POINTER;

    COMPtr<IMFAttributes> attributes;

    HRESULT hr = mixer->GetAttributes(&attributes);
    if (FAILED(hr))
        return hr;

    return attributes->SetBlob(VIDEO_ZOOM_RECT, (const UINT8*)&sourceRect, sizeof(sourceRect));
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::SetVideoPosition(const MFVideoNormalizedRect* pnrcSource, const LPRECT prcDest)
{
    LockHolder locker(m_lock);

    // First, check that the parameters are valid.

    if (!pnrcSource && !prcDest)
        return E_POINTER;

    if (pnrcSource) {
        if ((pnrcSource->left > pnrcSource->right) || (pnrcSource->top > pnrcSource->bottom))
            return E_INVALIDARG;

        // The source rectangle must be normalized.
        if ((pnrcSource->left < 0) || (pnrcSource->right > 1) || (pnrcSource->top < 0) || (pnrcSource->bottom > 1))
            return E_INVALIDARG;
    }

    if (prcDest) {
        if ((prcDest->left > prcDest->right) || (prcDest->top > prcDest->bottom))
            return E_INVALIDARG;
    }

    HRESULT hr = S_OK;

    // Set the source rectangle.
    if (pnrcSource) {
        m_sourceRect = *pnrcSource;

        if (m_mixer) {
            hr = setMixerSourceRect(m_mixer.get(), m_sourceRect);
            if (FAILED(hr))
                return hr;
        }
    }

    // Set the destination rectangle.
    if (prcDest) {
        RECT rcOldDest = m_presenterEngine->getDestinationRect();

        // If the destination rectangle hasn't changed, we are done.
        if (!EqualRect(&rcOldDest, prcDest)) {
            hr = m_presenterEngine->setDestinationRect(*prcDest);
            if (FAILED(hr))
                return hr;

            // We need to change the media type when the destination rectangle has changed.
            if (m_mixer) {
                hr = renegotiateMediaType();
                if (hr == MF_E_TRANSFORM_TYPE_NOT_SET) {
                    // This is not a critical failure; the EVR will let us know when
                    // we have to set the mixer media type.
                    hr = S_OK;
                } else {
                    if (FAILED(hr))
                        return hr;

                    // We have successfully changed the media type,
                    // ask for a repaint of the current frame.
                    m_repaint = true;
                    processOutput();
                }
            }
        }
    }

    return S_OK;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::GetVideoPosition(MFVideoNormalizedRect* pnrcSource, LPRECT prcDest)
{
    LockHolder locker(m_lock);

    if (!pnrcSource || !prcDest)
        return E_POINTER;

    *pnrcSource = m_sourceRect;
    *prcDest = m_presenterEngine->getDestinationRect();

    return S_OK;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::RepaintVideo()
{
    LockHolder locker(m_lock);

    HRESULT hr = checkShutdown();
    if (FAILED(hr))
        return hr;

    // Check that at least one sample has been presented.
    if (m_prerolled) {
        m_repaint = true;
        processOutput();
    }

    return S_OK;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::Invoke(IMFAsyncResult* pAsyncResult)
{
    return onSampleFree(pAsyncResult);
}

void MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::onMediaPlayerDeleted()
{
    m_mediaPlayer = nullptr;
}

void MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::paintCurrentFrame(GraphicsContext& context, const FloatRect& r)
{
    if (m_presenterEngine)
        m_presenterEngine->paintCurrentFrame(context, r);
}

float MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::currentTime()
{
    if (!m_clock)
        return 0.0f;

    LONGLONG clockTime;
    MFTIME systemTime;
    HRESULT hr = m_clock->GetCorrelatedTime(0, &clockTime, &systemTime);

    if (FAILED(hr))
        return 0.0f;

    // clockTime is in 100 nanoseconds, we need to convert to seconds.
    float currentTime = clockTime / tenMegahertz;

    if (currentTime > m_maxTimeLoaded)
        m_maxTimeLoaded = currentTime;

    return currentTime;
}

bool MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::isActive() const
{
    return ((m_renderState == RenderStateStarted) || (m_renderState == RenderStatePaused));
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::configureMixer(IMFTransform* mixer)
{
    COMPtr<IMFVideoDeviceID> videoDeviceID;
    HRESULT hr = mixer->QueryInterface(__uuidof(IMFVideoDeviceID), (void**)&videoDeviceID);
    if (FAILED(hr))
        return hr;

    IID deviceID = GUID_NULL;
    hr = videoDeviceID->GetDeviceID(&deviceID);
    if (FAILED(hr))
        return hr;

    // The mixer must have this device ID.
    if (!IsEqualGUID(deviceID, __uuidof(IDirect3DDevice9)))
        return MF_E_INVALIDREQUEST;

    setMixerSourceRect(mixer, m_sourceRect);

    return S_OK;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::flush()
{
    m_prerolled = false;

    // Flush the sceduler.
    // This call will block until the scheduler thread has finished flushing.
    m_scheduler.flush();

    if (m_renderState == RenderStateStopped)
        m_presenterEngine->presentSample(nullptr, 0);

    return S_OK;
}

static bool areMediaTypesEqual(IMFMediaType* type1, IMFMediaType* type2)
{
    if (!type1 && !type2)
        return true;
    if (!type1 || !type2)
        return false;

    DWORD flags = 0;
    return S_OK == type1->IsEqual(type2, &flags);
}

static FloatSize calculateNaturalSize(IMFMediaType* mediaType)
{
    UINT32 width = 0, height = 0;
    HRESULT hr = MFGetAttributeSize(mediaType, MF_MT_FRAME_SIZE, &width, &height);
    if (FAILED(hr) || !height)
        return FloatSize();

    UINT32 pixelAspectRatioNumerator = 0;
    UINT32 pixelAspectRatioDenominator = 0;
    hr = MFGetAttributeRatio(mediaType, MF_MT_PIXEL_ASPECT_RATIO, &pixelAspectRatioNumerator, &pixelAspectRatioDenominator);
    if (SUCCEEDED(hr) && pixelAspectRatioNumerator && pixelAspectRatioDenominator)
        return FloatSize(float(width) * pixelAspectRatioNumerator / pixelAspectRatioDenominator, height);

    return FloatSize();
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::setMediaType(IMFMediaType* mediaType)
{
    if (!mediaType) {
        m_mediaType = nullptr;
        releaseResources();
        return S_OK;
    }

    // If we have shut down, we cannot set the media type.
    HRESULT hr = checkShutdown();
    if (FAILED(hr)) {
        releaseResources();
        return hr;
    }

    if (areMediaTypesEqual(m_mediaType.get(), mediaType))
        return S_OK;

    m_mediaType = nullptr;
    releaseResources();

    // Get allocated samples from the presenter.
    VideoSampleList sampleQueue;
    hr = m_presenterEngine->createVideoSamples(mediaType, sampleQueue);
    if (FAILED(hr)) {
        releaseResources();
        return hr;
    }

    // Set the token counter on each sample.
    // This will help us to determine when they are invalid, and can be released.
    for (auto sample : sampleQueue) {
        hr = sample->SetUINT32(MFSamplePresenterSampleCounter, m_tokenCounter);
        if (FAILED(hr)) {
            releaseResources();
            return hr;
        }
    }

    // Add the samples to the sample pool.
    hr = m_samplePool.initialize(sampleQueue);
    if (FAILED(hr)) {
        releaseResources();
        return hr;
    }

    // Set the frame rate. 
    MFRatio fps = { 0, 0 };
    hr = MFGetAttributeRatio(mediaType, MF_MT_FRAME_RATE, (UINT32*)&fps.Numerator, (UINT32*)&fps.Denominator);
    if (SUCCEEDED(hr) && fps.Numerator && fps.Denominator)
        m_scheduler.setFrameRate(fps);
    else {
        // We could not get the frame ret, use default.
        const MFRatio defaultFrameRate = { 30, 1 };
        m_scheduler.setFrameRate(defaultFrameRate);
    }

    // Update natural size
    if (m_mediaPlayer)
        m_mediaPlayer->setNaturalSize(calculateNaturalSize(mediaType));

    ASSERT(mediaType);
    m_mediaType = mediaType;

    return S_OK;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::checkShutdown() const
{
    if (m_renderState == RenderStateShutdown)
        return MF_E_SHUTDOWN;
    return S_OK;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::renegotiateMediaType()
{
    HRESULT hr = S_OK;

    if (!m_mixer)
        return MF_E_INVALIDREQUEST;

    // Iterate over the available output types of the mixer.

    DWORD typeIndex = 0;
    bool foundMediaType = false;
    while (!foundMediaType && (hr != MF_E_NO_MORE_TYPES)) {
        // Get the next available media type.
        COMPtr<IMFMediaType> mixerType;
        hr = m_mixer->GetOutputAvailableType(0, typeIndex++, &mixerType);
        if (FAILED(hr))
            break;

        // Do we support this media type?
        hr = isMediaTypeSupported(mixerType.get());
        if (FAILED(hr))
            break;

        // Make adjustments to proposed media type.
        COMPtr<IMFMediaType> optimalType;
        hr = createOptimalVideoType(mixerType.get(), &optimalType);
        if (FAILED(hr))
            break;

        // Test whether the mixer can accept the modified media type
        hr = m_mixer->SetOutputType(0, optimalType.get(), MFT_SET_TYPE_TEST_ONLY);
        if (FAILED(hr))
            break;

        // Try to set the new media type

        hr = setMediaType(optimalType.get());
        if (FAILED(hr))
            break;

        hr = m_mixer->SetOutputType(0, optimalType.get(), 0);

        ASSERT(SUCCEEDED(hr));

        if (FAILED(hr))
            setMediaType(nullptr);
        else
            foundMediaType = true;
    }

    return hr;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::processInputNotify()
{
    // We have a new sample.
    m_sampleNotify = true;

    if (!m_mediaType) {
        // The media type is not valid.
        return MF_E_TRANSFORM_TYPE_NOT_SET;
    } 
    
    // Invalidate the video area
    if (m_mediaPlayer) {
        callOnMainThread([weakPtr = m_mediaPlayer->m_weakThis] {
            if (weakPtr)
                weakPtr->invalidateFrameView();
        });
    }

    // Process sample
    processOutputLoop();

    return S_OK;
}

static float MFOffsetToFloat(const MFOffset& offset)
{
    const int denominator = std::numeric_limits<WORD>::max() + 1;
    return offset.value + (float(offset.fract) / denominator);
}

static MFOffset MakeOffset(float v)
{
    // v = offset.value + (offset.fract / denominator), where denominator = 65536.0f.
    const int denominator = std::numeric_limits<WORD>::max() + 1;
    MFOffset offset;
    offset.value = short(v);
    offset.fract = WORD(denominator * (v - offset.value));
    return offset;
}

static MFVideoArea MakeArea(float x, float y, DWORD width, DWORD height)
{
    MFVideoArea area;
    area.OffsetX = MakeOffset(x);
    area.OffsetY = MakeOffset(y);
    area.Area.cx = width;
    area.Area.cy = height;
    return area;
}

static HRESULT validateVideoArea(const MFVideoArea& area, UINT32 width, UINT32 height)
{
    float fOffsetX = MFOffsetToFloat(area.OffsetX);
    float fOffsetY = MFOffsetToFloat(area.OffsetY);

    if (((LONG)fOffsetX + area.Area.cx > width) || ((LONG)fOffsetY + area.Area.cy > height))
        return MF_E_INVALIDMEDIATYPE;
    return S_OK;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::beginStreaming()
{
    return m_scheduler.startScheduler(m_clock.get());
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::endStreaming()
{
    return m_scheduler.stopScheduler();
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::checkEndOfStream()
{
    if (!m_endStreaming) {
        // We have not received the end-of-stream message from the EVR.
        return S_OK;
    }

    if (m_sampleNotify) {
        // There is still input samples available for the mixer. 
        return S_OK;
    }

    if (m_samplePool.areSamplesPending()) {
        // There are samples scheduled for rendering.
        return S_OK;
    }

    // We are done, notify the EVR.
    notifyEvent(EC_COMPLETE, (LONG_PTR)S_OK, 0);
    m_endStreaming = false;
    return S_OK;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::isMediaTypeSupported(IMFMediaType* mediaType)
{
    COMPtr<IMFMediaType> proposedVideoType = mediaType;

    // We don't support compressed media types.
    BOOL compressed = FALSE;
    HRESULT hr = proposedVideoType->IsCompressedFormat(&compressed);
    if (FAILED(hr))
        return hr;
    if (compressed)
        return MF_E_INVALIDMEDIATYPE;

    // Validate the format.
    GUID guidSubType = GUID_NULL;
    hr = proposedVideoType->GetGUID(MF_MT_SUBTYPE, &guidSubType);
    if (FAILED(hr))
        return hr;
    D3DFORMAT d3dFormat = (D3DFORMAT)guidSubType.Data1;

    // Check if the format can be used as backbuffer format.
    hr = m_presenterEngine->checkFormat(d3dFormat);
    if (FAILED(hr))
        return hr;

    // Check interlaced formats.
    MFVideoInterlaceMode interlaceMode = MFVideoInterlace_Unknown;
    hr = proposedVideoType->GetUINT32(MF_MT_INTERLACE_MODE, (UINT32*)&interlaceMode);
    if (FAILED(hr))
        return hr;

    if (interlaceMode != MFVideoInterlace_Progressive)
        return MF_E_INVALIDMEDIATYPE;

    UINT32 width = 0, height = 0;
    hr = MFGetAttributeSize(proposedVideoType.get(), MF_MT_FRAME_SIZE, &width, &height);
    if (FAILED(hr))
        return hr;

    // Validate apertures.
    MFVideoArea videoCropArea;
    if (SUCCEEDED(proposedVideoType->GetBlob(MF_MT_PAN_SCAN_APERTURE, (UINT8*)&videoCropArea, sizeof(MFVideoArea), nullptr)))
        validateVideoArea(videoCropArea, width, height);
    if (SUCCEEDED(proposedVideoType->GetBlob(MF_MT_GEOMETRIC_APERTURE, (UINT8*)&videoCropArea, sizeof(MFVideoArea), nullptr)))
        validateVideoArea(videoCropArea, width, height);
    if (SUCCEEDED(proposedVideoType->GetBlob(MF_MT_MINIMUM_DISPLAY_APERTURE, (UINT8*)&videoCropArea, sizeof(MFVideoArea), nullptr)))
        validateVideoArea(videoCropArea, width, height);
    
    return S_OK;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::createOptimalVideoType(IMFMediaType* proposedType, IMFMediaType** optimalType)
{
    COMPtr<IMFMediaType> optimalVideoType;
    HRESULT hr = MFCreateMediaType(&optimalVideoType);
    if (FAILED(hr))
        return hr;
    hr = optimalVideoType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
    if (FAILED(hr))
        return hr;

    hr = proposedType->CopyAllItems(optimalVideoType.get());
    if (FAILED(hr))
        return hr;

    // We now modify the new media type.

    // We assume that the monitor's pixel aspect ratio is 1:1,
    // and that the pixel aspect ratio is preserved by the presenter.
    hr = MFSetAttributeRatio(optimalVideoType.get(), MF_MT_PIXEL_ASPECT_RATIO, 1, 1);
    if (FAILED(hr))
        return hr;

    // Get the output rectangle.
    RECT rcOutput = m_presenterEngine->getDestinationRect();
    if (IsRectEmpty(&rcOutput)) {
        hr = calculateOutputRectangle(proposedType, rcOutput);
        if (FAILED(hr))
            return hr;
    }

    hr = optimalVideoType->SetUINT32(MF_MT_YUV_MATRIX, MFVideoTransferMatrix_BT709);
    if (FAILED(hr))
        return hr;

    hr = optimalVideoType->SetUINT32(MF_MT_TRANSFER_FUNCTION, MFVideoTransFunc_709);
    if (FAILED(hr))
        return hr;

    hr = optimalVideoType->SetUINT32(MF_MT_VIDEO_PRIMARIES, MFVideoPrimaries_BT709);
    if (FAILED(hr))
        return hr;

    hr = optimalVideoType->SetUINT32(MF_MT_VIDEO_NOMINAL_RANGE, MFNominalRange_16_235);
    if (FAILED(hr))
        return hr;

    hr = optimalVideoType->SetUINT32(MF_MT_VIDEO_LIGHTING, MFVideoLighting_dim);
    if (FAILED(hr))
        return hr;

    hr = MFSetAttributeSize(optimalVideoType.get(), MF_MT_FRAME_SIZE, rcOutput.right, rcOutput.bottom);
    if (FAILED(hr))
        return hr;

    MFVideoArea displayArea = MakeArea(0, 0, rcOutput.right, rcOutput.bottom);

    hr = optimalVideoType->SetUINT32(MF_MT_PAN_SCAN_ENABLED, FALSE);
    if (FAILED(hr))
        return hr;

    hr = optimalVideoType->SetBlob(MF_MT_GEOMETRIC_APERTURE, (UINT8*)&displayArea, sizeof(MFVideoArea));
    if (FAILED(hr))
        return hr;

    hr = optimalVideoType->SetBlob(MF_MT_PAN_SCAN_APERTURE, (UINT8*)&displayArea, sizeof(MFVideoArea));
    if (FAILED(hr))
        return hr;

    hr = optimalVideoType->SetBlob(MF_MT_MINIMUM_DISPLAY_APERTURE, (UINT8*)&displayArea, sizeof(MFVideoArea));
    if (FAILED(hr))
        return hr;

    *optimalType = optimalVideoType.leakRef();

    return S_OK;
}

static RECT correctAspectRatio(const RECT& src, const MFRatio& srcPAR, const MFRatio& destPAR)
{
    RECT rc = { 0, 0, src.right - src.left, src.bottom - src.top };

    if ((srcPAR.Numerator * destPAR.Denominator) != (srcPAR.Denominator * destPAR.Numerator)) {
        // The source and destination aspect ratios are different

        // Transform the source aspect ratio to 1:1
        if (srcPAR.Numerator > srcPAR.Denominator)
            rc.right = MulDiv(rc.right, srcPAR.Numerator, srcPAR.Denominator);
        else if (srcPAR.Numerator < srcPAR.Denominator)
            rc.bottom = MulDiv(rc.bottom, srcPAR.Denominator, srcPAR.Numerator);


        // Transform to destination aspect ratio.
        if (destPAR.Numerator > destPAR.Denominator)
            rc.bottom = MulDiv(rc.bottom, destPAR.Numerator, destPAR.Denominator);
        else if (destPAR.Numerator < destPAR.Denominator)
            rc.right = MulDiv(rc.right, destPAR.Denominator, destPAR.Numerator);

    }

    return rc;
}

static HRESULT GetVideoDisplayArea(IMFMediaType* type, MFVideoArea* area)
{
    if (!type || !area)
        return E_POINTER;

    HRESULT hr = S_OK;
    UINT32 width = 0, height = 0;

    BOOL bPanScan = MFGetAttributeUINT32(type, MF_MT_PAN_SCAN_ENABLED, FALSE);

    if (bPanScan)
        hr = type->GetBlob(MF_MT_PAN_SCAN_APERTURE, (UINT8*)area, sizeof(MFVideoArea), nullptr);

    if (!bPanScan || hr == MF_E_ATTRIBUTENOTFOUND) {
        hr = type->GetBlob(MF_MT_MINIMUM_DISPLAY_APERTURE, (UINT8*)area, sizeof(MFVideoArea), nullptr);

        if (hr == MF_E_ATTRIBUTENOTFOUND)
            hr = type->GetBlob(MF_MT_GEOMETRIC_APERTURE, (UINT8*)area, sizeof(MFVideoArea), nullptr);

        if (hr == MF_E_ATTRIBUTENOTFOUND) {
            hr = MFGetAttributeSize(type, MF_MT_FRAME_SIZE, &width, &height);
            if (SUCCEEDED(hr))
                *area = MakeArea(0.0, 0.0, width, height);
        }
    }

    return hr;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::calculateOutputRectangle(IMFMediaType* proposedType, RECT& outputRect)
{
    COMPtr<IMFMediaType> proposedVideoType = proposedType;

    UINT32 srcWidth = 0, srcHeight = 0;
    HRESULT hr = MFGetAttributeSize(proposedVideoType.get(), MF_MT_FRAME_SIZE, &srcWidth, &srcHeight);
    if (FAILED(hr))
        return hr;

    MFVideoArea displayArea;
    ZeroMemory(&displayArea, sizeof(displayArea));

    hr = GetVideoDisplayArea(proposedVideoType.get(), &displayArea);
    if (FAILED(hr))
        return hr;

    LONG offsetX = (LONG)MFOffsetToFloat(displayArea.OffsetX);
    LONG offsetY = (LONG)MFOffsetToFloat(displayArea.OffsetY);

    // Check if the display area is valid.
    // If it is valid, we use it. If not, we use the frame dimensions.

    RECT rcOutput;

    if (displayArea.Area.cx != 0
        && displayArea.Area.cy != 0
        && offsetX + displayArea.Area.cx <= srcWidth
        && offsetY + displayArea.Area.cy <= srcHeight) {
        rcOutput.left = offsetX;
        rcOutput.right = offsetX + displayArea.Area.cx;
        rcOutput.top = offsetY;
        rcOutput.bottom = offsetY + displayArea.Area.cy;
    } else {
        rcOutput.left = 0;
        rcOutput.top = 0;
        rcOutput.right = srcWidth;
        rcOutput.bottom = srcHeight;
    }

    // Correct aspect ratio.

    MFRatio inputPAR = { 1, 1 };
    MFRatio outputPAR = { 1, 1 }; // We assume the monitor's pixels are square.
    MFGetAttributeRatio(proposedVideoType.get(), MF_MT_PIXEL_ASPECT_RATIO, (UINT32*)&inputPAR.Numerator, (UINT32*)&inputPAR.Denominator);
    outputRect = correctAspectRatio(rcOutput, inputPAR, outputPAR);

    return S_OK;
}

void MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::processOutputLoop()
{
    // Get video frames from the mixer and schedule them for presentation.
    HRESULT hr = S_OK;

    while (hr == S_OK) {
        if (!m_sampleNotify) {
            // Currently no more input samples.
            hr = MF_E_TRANSFORM_NEED_MORE_INPUT;
            break;
        }

        // We break from the loop if we fail to process a sample.
        hr = processOutput();
    }

    if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT)
        checkEndOfStream();
}

static HRESULT setDesiredSampleTime(IMFSample* sample, const LONGLONG& sampleTime, const LONGLONG& duration)
{
    // To tell the mixer to give us an earlier frame for repainting, we can set the desired sample time.
    // We have to clear the desired sample time before reusing the sample.

    if (!sample)
        return E_POINTER;

    COMPtr<IMFDesiredSample> desired;

    HRESULT hr = sample->QueryInterface(__uuidof(IMFDesiredSample), (void**)&desired);

    if (SUCCEEDED(hr))
        desired->SetDesiredSampleTimeAndDuration(sampleTime, duration);

    return hr;
}

static HRESULT clearDesiredSampleTime(IMFSample* sample)
{
    if (!sample)
        return E_POINTER;

    // We need to retrieve some attributes we have set on the sample before we call 
    // IMFDesiredSample::Clear(), and set them once more, since they are cleared by
    // the Clear() call.

    UINT32 counter = MFGetAttributeUINT32(sample, MFSamplePresenterSampleCounter, (UINT32)-1);

    COMPtr<IMFDesiredSample> desired;
    HRESULT hr = sample->QueryInterface(__uuidof(IMFDesiredSample), (void**)&desired);
    if (SUCCEEDED(hr)) {
        desired->Clear();

        hr = sample->SetUINT32(MFSamplePresenterSampleCounter, counter);
        if (FAILED(hr))
            return hr;
    }

    return hr;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::processOutput()
{
    // This method will try to get a new sample from the mixer.
    // It is called when the mixer has a new sample, or when repainting the last frame.

    ASSERT(m_sampleNotify || m_repaint);

    LONGLONG mixerStartTime = 0, mixerEndTime = 0;
    MFTIME systemTime = 0;
    bool repaint = m_repaint;  

    // If the clock has not started, we only present the first sample. 

    if ((m_renderState != RenderStateStarted) && !m_repaint && m_prerolled)
        return S_FALSE;

    if (!m_mixer)
        return MF_E_INVALIDREQUEST;

    // Get a free sample from the pool.
    COMPtr<IMFSample> sample;
    HRESULT hr = m_samplePool.getSample(sample);
    if (hr == MF_E_SAMPLEALLOCATOR_EMPTY)
        return S_FALSE; // We will try again later when there are free samples

    if (FAILED(hr))
        return hr;

    ASSERT(sample);

    ASSERT(MFGetAttributeUINT32(sample.get(), MFSamplePresenterSampleCounter, (UINT32)-1) == m_tokenCounter);

    if (m_repaint) {
        // Get the most recent sample from the mixer.
        setDesiredSampleTime(sample.get(), m_scheduler.lastSampleTime(), m_scheduler.frameDuration());
        m_repaint = false;
    } else {
        // Clear the desired sample time to get the next sample in the stream.
        clearDesiredSampleTime(sample.get());

        if (m_clock) {
            // Get the starting time of the ProcessOutput call.
            m_clock->GetCorrelatedTime(0, &mixerStartTime, &systemTime);
        }
    }

    // Get a sample from the mixer. 
    MFT_OUTPUT_DATA_BUFFER dataBuffer;
    ZeroMemory(&dataBuffer, sizeof(dataBuffer));

    dataBuffer.dwStreamID = 0;
    dataBuffer.pSample = sample.get();
    dataBuffer.dwStatus = 0;

    DWORD status = 0;
    hr = m_mixer->ProcessOutput(0, 1, &dataBuffer, &status);

    // Release events. There are usually no events returned,
    // but in case there are, we should release them.
    if (dataBuffer.pEvents)
        dataBuffer.pEvents->Release();

    if (FAILED(hr)) {
        HRESULT hr2 = m_samplePool.returnSample(sample.get());
        if (FAILED(hr2))
            return hr2;

        if (hr == MF_E_TRANSFORM_TYPE_NOT_SET) {
            // The media type has not been set, renegotiate.
            hr = renegotiateMediaType();
        } else if (hr == MF_E_TRANSFORM_STREAM_CHANGE) {
            // The media type changed, reset it.
            setMediaType(nullptr);
        } else if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT) {
            // The mixer needs more input.
            m_sampleNotify = false;
        }
    } else {
        // We have got a sample from the mixer.

        if (m_clock && !repaint) {
            // Notify the EVR about latency.
            m_clock->GetCorrelatedTime(0, &mixerEndTime, &systemTime);

            LONGLONG latencyTime = mixerEndTime - mixerStartTime;
            notifyEvent(EC_PROCESSING_LATENCY, (LONG_PTR)&latencyTime, 0);
        }

        // Make sure we are notified when the sample is released
        hr = trackSample(sample.get());
        if (FAILED(hr))
            return hr;

        // Deliver the sample for scheduling
        hr = deliverSample(sample.get(), repaint);
        if (FAILED(hr))
            return hr;

        // At least one sample has been presented now.
        m_prerolled = true;
    }

    return hr;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::deliverSample(IMFSample* sample, bool repaint)
{
    if (!sample)
        return E_POINTER;

    Direct3DPresenter::DeviceState state = Direct3DPresenter::DeviceOK;

    // Determine if the sample should be presented immediately.
    bool presentNow = ((m_renderState != RenderStateStarted) || isScrubbing() || repaint);

    HRESULT hr = m_presenterEngine->checkDeviceState(state);

    if (SUCCEEDED(hr))
        hr = m_scheduler.scheduleSample(sample, presentNow);

    if (FAILED(hr)) {
        // Streaming has failed, notify the EVR.
        notifyEvent(EC_ERRORABORT, hr, 0);
    } else if (state == Direct3DPresenter::DeviceReset)
        notifyEvent(EC_DISPLAY_CHANGED, S_OK, 0);

    return hr;
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::trackSample(IMFSample* sample)
{
    if (!sample)
        return E_POINTER;

    COMPtr<IMFTrackedSample> tracked;

    HRESULT hr = sample->QueryInterface(__uuidof(IMFTrackedSample), (void**)&tracked);
    if (FAILED(hr))
        return hr;

    if (!tracked)
        return E_POINTER;

    // Set callback object on which the onSampleFree method is invoked when the sample is no longer used.
    return tracked->SetAllocator(this, nullptr);
}

void MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::releaseResources()
{
    // The token counter is incremented to indicate that existing samples are
    // invalid and can be disposed in the method onSampleFree.
    m_tokenCounter++;

    flush();

    m_samplePool.clear();

    if (m_presenterEngine)
        m_presenterEngine->releaseResources();
}

HRESULT MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::onSampleFree(IMFAsyncResult* result)
{
    if (!result)
        return E_POINTER;

    COMPtr<IUnknown> object;
    HRESULT hr = result->GetObject(&object);
    if (FAILED(hr)) {
        notifyEvent(EC_ERRORABORT, hr, 0);
        return hr;
    }

    COMPtr<IMFSample> sample;
    hr = object->QueryInterface(__uuidof(IMFSample), (void**)&sample);
    if (FAILED(hr)) {
        notifyEvent(EC_ERRORABORT, hr, 0);
        return hr;
    }

    m_lock.lock();

    if (MFGetAttributeUINT32(sample.get(), MFSamplePresenterSampleCounter, (UINT32)-1) == m_tokenCounter) {
        hr = m_samplePool.returnSample(sample.get());

        // Do more processing, since a free sample is available
        if (SUCCEEDED(hr))
            processOutputLoop();
    }

    m_lock.unlock();

    if (FAILED(hr))
        notifyEvent(EC_ERRORABORT, hr, 0);

    return hr;
}

void MediaPlayerPrivateMediaFoundation::CustomVideoPresenter::notifyEvent(long EventCode, LONG_PTR Param1, LONG_PTR Param2)
{
    if (m_mediaEventSink)
        m_mediaEventSink->Notify(EventCode, Param1, Param2);
}

HRESULT MediaPlayerPrivateMediaFoundation::VideoSamplePool::getSample(COMPtr<IMFSample>& sample)
{
    LockHolder locker(m_lock);

    if (!m_initialized)
        return MF_E_NOT_INITIALIZED;

    if (m_videoSampleQueue.isEmpty())
        return MF_E_SAMPLEALLOCATOR_EMPTY;

    sample = m_videoSampleQueue.takeFirst();

    m_pending++;

    return S_OK;
}

HRESULT MediaPlayerPrivateMediaFoundation::VideoSamplePool::returnSample(IMFSample* sample)
{
    if (!sample)
        return E_POINTER;

    LockHolder locker(m_lock);

    if (!m_initialized)
        return MF_E_NOT_INITIALIZED;

    m_videoSampleQueue.append(sample);
    m_pending--;
    return S_OK;
}

bool MediaPlayerPrivateMediaFoundation::VideoSamplePool::areSamplesPending()
{
    LockHolder locker(m_lock);

    if (!m_initialized)
        return FALSE;

    return (m_pending > 0);
}

HRESULT MediaPlayerPrivateMediaFoundation::VideoSamplePool::initialize(VideoSampleList& samples)
{
    LockHolder locker(m_lock);

    if (m_initialized)
        return MF_E_INVALIDREQUEST;

    // Copy the samples
    for (auto sample : samples)
        m_videoSampleQueue.append(sample);

    m_initialized = true;
    samples.clear();

    return S_OK;
}

void MediaPlayerPrivateMediaFoundation::VideoSamplePool::clear()
{
    LockHolder locker(m_lock);

    m_videoSampleQueue.clear();
    m_initialized = false;
    m_pending = 0;
}


// Scheduler thread messages.

enum ScheduleEvent {
    EventTerminate = WM_USER,
    EventSchedule,
    EventFlush
};

void MediaPlayerPrivateMediaFoundation::VideoScheduler::setFrameRate(const MFRatio& fps)
{
    UINT64 avgTimePerFrame = 0;
    MFFrameRateToAverageTimePerFrame(fps.Numerator, fps.Denominator, &avgTimePerFrame);

    m_frameDuration = (MFTIME)avgTimePerFrame;
}

HRESULT MediaPlayerPrivateMediaFoundation::VideoScheduler::startScheduler(IMFClock* clock)
{
    if (m_schedulerThread.isValid())
        return E_UNEXPECTED;

    HRESULT hr = S_OK;

    m_clock = clock;

    // Use high timer resolution.
    timeBeginPeriod(1);

    // Create an event to signal that the scheduler thread has started.
    m_threadReadyEvent = ::CreateEvent(nullptr, FALSE, FALSE, nullptr);
    if (!m_threadReadyEvent.isValid())
        return HRESULT_FROM_WIN32(GetLastError());

    // Create an event to signal that the flush has completed.
    m_flushEvent = ::CreateEvent(nullptr, FALSE, FALSE, nullptr);
    if (!m_flushEvent.isValid())
        return HRESULT_FROM_WIN32(GetLastError());

    // Start scheduler thread.
    DWORD threadID = 0;
    m_schedulerThread = ::CreateThread(nullptr, 0, schedulerThreadProc, (LPVOID)this, 0, &threadID);
    if (!m_schedulerThread.isValid())
        return HRESULT_FROM_WIN32(GetLastError());

    HANDLE hObjects[] = { m_threadReadyEvent.get(), m_schedulerThread.get() };

    // Wait for the thread to start
    DWORD result = ::WaitForMultipleObjects(2, hObjects, FALSE, INFINITE);
    if (WAIT_OBJECT_0 != result) {
        // The thread has terminated.
        m_schedulerThread.clear();
        return E_UNEXPECTED;
    }

    m_threadID = threadID;

    return hr;
}

HRESULT MediaPlayerPrivateMediaFoundation::VideoScheduler::stopScheduler()
{
    if (!m_schedulerThread.isValid())
        return S_OK;

    // Terminate the scheduler thread
    stopThread();
    ::PostThreadMessage(m_threadID, EventTerminate, 0, 0);

    // Wait for the scheduler thread to finish.
    ::WaitForSingleObject(m_schedulerThread.get(), INFINITE);

    LockHolder locker(m_lock);

    m_scheduledSamples.clear();
    m_schedulerThread.clear();
    m_flushEvent.clear();

    // Clear previously set timer resolution.
    timeEndPeriod(1);

    return S_OK;
}

HRESULT MediaPlayerPrivateMediaFoundation::VideoScheduler::flush()
{
    // This method will wait for the flush to finish on the worker thread.

    if (m_schedulerThread.isValid()) {
        ::PostThreadMessage(m_threadID, EventFlush, 0, 0);

        HANDLE objects[] = { m_flushEvent.get(), m_schedulerThread.get() };

        const int schedulerTimeout = 5000;

        // Wait for the flush to finish or the thread to terminate.
        ::WaitForMultipleObjects(2, objects, FALSE, schedulerTimeout);
    }

    return S_OK;
}

HRESULT MediaPlayerPrivateMediaFoundation::VideoScheduler::scheduleSample(IMFSample* sample, bool presentNow)
{
    if (!sample)
        return E_POINTER;

    if (!m_presenter)
        return MF_E_NOT_INITIALIZED;

    if (!m_schedulerThread.isValid())
        return MF_E_NOT_INITIALIZED;

    DWORD exitCode = 0;
    ::GetExitCodeThread(m_schedulerThread.get(), &exitCode);

    if (exitCode != STILL_ACTIVE)
        return E_FAIL;

    if (presentNow || !m_clock)
        m_presenter->presentSample(sample, 0);
    else {
        // Submit the sample for scheduling.
        LockHolder locker(m_lock);
        m_scheduledSamples.append(sample);

        ::PostThreadMessage(m_threadID, EventSchedule, 0, 0);
    }

    return S_OK;
}

HRESULT MediaPlayerPrivateMediaFoundation::VideoScheduler::processSamplesInQueue(LONG& nextSleep)
{
    HRESULT hr = S_OK;
    LONG wait = 0;

    // Process samples as long as there are samples in the queue, and they have not arrived too early.

    while (!m_exitThread) {
        COMPtr<IMFSample> sample;

        if (true) {
            LockHolder locker(m_lock);
            if (m_scheduledSamples.isEmpty())
                break;
            sample = m_scheduledSamples.takeFirst();
        }

        // Process the sample.
        // If the sample has arrived too early, wait will be > 0,
        // and the scheduler should go to sleep.
        hr = processSample(sample.get(), wait);

        if (FAILED(hr))
            break;

        if (wait > 0)
            break;
    }

    if (!wait) {
        // The queue is empty. Sleep until the next message arrives.
        wait = INFINITE;
    }

    nextSleep = wait;
    return hr;
}

// MFTimeToMilliseconds: Convert 100-nanosecond time to milliseconds.
static LONG MFTimeToMilliseconds(const LONGLONG& time)
{
    return (time / 10000);
}

HRESULT MediaPlayerPrivateMediaFoundation::VideoScheduler::processSample(IMFSample* sample, LONG& nextSleep)
{
    if (!sample)
        return E_POINTER;

    HRESULT hr = S_OK;

    LONGLONG presentationTime = 0;
    LONGLONG timeNow = 0;
    MFTIME systemTime = 0;

    bool presentNow = true;
    LONG nextSleepTime = 0;

    if (m_clock) {
        // Get the time stamp of the sample.
        // A sample can possibly have no time stamp.
        hr = sample->GetSampleTime(&presentationTime);

        // Get the clock time.
        // If the sample does not have a time stamp, the clock time is not needed.
        if (SUCCEEDED(hr))
            hr = m_clock->GetCorrelatedTime(0, &timeNow, &systemTime);

        // Determine the time until the sample should be presented.
        // Samples arriving late, will have negative values.
        LONGLONG timeDelta = presentationTime - timeNow;
        if (m_playbackRate < 0) {
            // Reverse delta for reverse playback.
            timeDelta = -timeDelta;
        }

        LONGLONG frameDurationOneFourth = m_frameDuration / 4;

        if (timeDelta < -frameDurationOneFourth) {
            // The sample has arrived late. 
            presentNow = true;
        } else if (timeDelta > (3 * frameDurationOneFourth)) {
            // We can sleep, the sample has arrived too early.
            nextSleepTime = MFTimeToMilliseconds(timeDelta - (3 * frameDurationOneFourth));

            // Since sleeping is using the system clock, we need to convert the sleep time
            // from presentation time to system time.
            nextSleepTime = (LONG)(nextSleepTime / fabsf(m_playbackRate));

            presentNow = false;
        }
    }

    if (presentNow)
        hr = m_presenter->presentSample(sample, presentationTime);
    else {
        // Return the sample to the queue, since it is not ready.
        LockHolder locker(m_lock);
        m_scheduledSamples.prepend(sample);
    }

    nextSleep = nextSleepTime;

    return hr;
}

DWORD WINAPI MediaPlayerPrivateMediaFoundation::VideoScheduler::schedulerThreadProc(LPVOID lpParameter)
{
    VideoScheduler* scheduler = reinterpret_cast<VideoScheduler*>(lpParameter);
    if (!scheduler)
        return static_cast<DWORD>(-1);
    return scheduler->schedulerThreadProcPrivate();
}

DWORD MediaPlayerPrivateMediaFoundation::VideoScheduler::schedulerThreadProcPrivate()
{
    HRESULT hr = S_OK;

    // This will force a message queue to be created for the thread.
    MSG msg;
    PeekMessage(&msg, nullptr, WM_USER, WM_USER, PM_NOREMOVE);

    // The thread is ready.
    SetEvent(m_threadReadyEvent.get());

    LONG wait = INFINITE;
    m_exitThread = false;
    while (!m_exitThread) {
        // Wait for messages
        DWORD result = MsgWaitForMultipleObjects(0, nullptr, FALSE, wait, QS_POSTMESSAGE);

        if (result == WAIT_TIMEOUT) {
            hr = processSamplesInQueue(wait);
            if (FAILED(hr))
                m_exitThread = true;
        }

        while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
            bool processSamples = true;

            switch (msg.message) {
            case EventTerminate:
                m_exitThread = true;
                break;

            case EventFlush:
                {
                    LockHolder lock(m_lock);
                    m_scheduledSamples.clear();
                }
                wait = INFINITE;
                SetEvent(m_flushEvent.get());
                break;

            case EventSchedule:
                if (processSamples) {
                    hr = processSamplesInQueue(wait);
                    if (FAILED(hr))
                        m_exitThread = true;
                    processSamples = (wait != INFINITE);
                }
                break;
            }
        }
    }
    return (SUCCEEDED(hr) ? 0 : 1);
}

static HRESULT findAdapter(IDirect3D9* direct3D9, HMONITOR monitor, UINT& adapterID)
{
    HRESULT hr = E_FAIL;

    UINT adapterCount = direct3D9->GetAdapterCount();
    for (UINT i = 0; i < adapterCount; i++) {
        HMONITOR monitorTmp = direct3D9->GetAdapterMonitor(i);

        if (!monitorTmp)
            break;

        if (monitorTmp == monitor) {
            adapterID = i;
            hr = S_OK;
            break;
        }
    }

    return hr;
}

MediaPlayerPrivateMediaFoundation::Direct3DPresenter::Direct3DPresenter()
{
    SetRectEmpty(&m_destRect);

    ZeroMemory(&m_displayMode, sizeof(m_displayMode));

    HRESULT hr = initializeD3D();

    if (FAILED(hr))
        return;

    createD3DDevice();
}

MediaPlayerPrivateMediaFoundation::Direct3DPresenter::~Direct3DPresenter() = default;

HRESULT MediaPlayerPrivateMediaFoundation::Direct3DPresenter::getService(REFGUID guidService, REFIID riid, void** ppv)
{
    ASSERT(ppv);

    HRESULT hr = S_OK;

    if (riid == __uuidof(IDirect3DDeviceManager9)) {
        if (!m_deviceManager)
            hr = MF_E_UNSUPPORTED_SERVICE;
        else {
            *ppv = m_deviceManager.get();
            m_deviceManager->AddRef();
        }
    } else
        hr = MF_E_UNSUPPORTED_SERVICE;

    return hr;
}

HRESULT MediaPlayerPrivateMediaFoundation::Direct3DPresenter::checkFormat(D3DFORMAT format)
{
    HRESULT hr = S_OK;

    UINT adapter = D3DADAPTER_DEFAULT;
    D3DDEVTYPE type = D3DDEVTYPE_HAL;

    if (m_device) {
        D3DDEVICE_CREATION_PARAMETERS params;
        hr = m_device->GetCreationParameters(&params);
        if (FAILED(hr))
            return hr;

        adapter = params.AdapterOrdinal;
        type = params.DeviceType;
    }

    D3DDISPLAYMODE mode;
    hr = m_direct3D9->GetAdapterDisplayMode(adapter, &mode);
    if (FAILED(hr))
        return hr;

    return m_direct3D9->CheckDeviceType(adapter, type, mode.Format, format, TRUE);
}

HRESULT MediaPlayerPrivateMediaFoundation::Direct3DPresenter::setVideoWindow(HWND hwnd)
{
    ASSERT(IsWindow(hwnd));
    ASSERT(hwnd != m_hwnd);

    {
        LockHolder locker(m_lock);
        m_hwnd = hwnd;
    }

    return createD3DDevice();
}

HRESULT MediaPlayerPrivateMediaFoundation::Direct3DPresenter::setDestinationRect(const RECT& rcDest)
{
    if (EqualRect(&rcDest, &m_destRect))
        return S_OK;

    LockHolder locker(m_lock);

    m_destRect = rcDest;

    return S_OK;
}

HRESULT MediaPlayerPrivateMediaFoundation::Direct3DPresenter::createVideoSamples(IMFMediaType* format, VideoSampleList& videoSampleQueue)
{
    // Create video samples matching the supplied format.
    // A swap chain with a single back buffer will be created for each video sample.
    // The mixer will render to the back buffer through a surface kept by the sample.
    // The surface can be rendered to a window by presenting the swap chain.
    // In our case the surface is transferred to system memory, and rendered to a graphics context.

    if (!m_hwnd)
        return MF_E_INVALIDREQUEST;

    if (!format)
        return MF_E_UNEXPECTED;


    LockHolder locker(m_lock);

    releaseResources();

    D3DPRESENT_PARAMETERS presentParameters;
    HRESULT hr = getSwapChainPresentParameters(format, &presentParameters);
    if (FAILED(hr)) {
        releaseResources();
        return hr;
    }

    static const int presenterBufferCount = 3;

    for (int i = 0; i < presenterBufferCount; i++) {
        COMPtr<IDirect3DSwapChain9> swapChain;
        hr = m_device->CreateAdditionalSwapChain(&presentParameters, &swapChain);
        if (FAILED(hr)) {
            releaseResources();
            return hr;
        }

        COMPtr<IMFSample> videoSample;
        hr = createD3DSample(swapChain.get(), videoSample);
        if (FAILED(hr)) {
            releaseResources();
            return hr;
        }

        videoSampleQueue.append(videoSample);
    }

    return hr;
}

void MediaPlayerPrivateMediaFoundation::Direct3DPresenter::releaseResources()
{
    m_surfaceRepaint = nullptr;
}

HRESULT MediaPlayerPrivateMediaFoundation::Direct3DPresenter::checkDeviceState(DeviceState& state)
{
    LockHolder locker(m_lock);

    HRESULT hr = m_device->CheckDeviceState(m_hwnd);

    state = DeviceOK;

    // Not all failure codes are critical.

    switch (hr) {
    case S_OK:
    case S_PRESENT_OCCLUDED:
    case S_PRESENT_MODE_CHANGED:
        hr = S_OK;
        break;

    case D3DERR_DEVICELOST:
    case D3DERR_DEVICEHUNG:
        hr = createD3DDevice();
        if (FAILED(hr))
            return hr;
        state = DeviceReset;
        hr = S_OK;
        break;

    case D3DERR_DEVICEREMOVED:
        state = DeviceRemoved;
        break;

    case E_INVALIDARG:
        // This might happen if the window has been destroyed, or is not valid.
        // A new device will be created if a new window is set.
        hr = S_OK;
    }

    return hr;
}

HRESULT MediaPlayerPrivateMediaFoundation::Direct3DPresenter::presentSample(IMFSample* sample, LONGLONG targetPresentationTime)
{
    HRESULT hr = S_OK;

    LockHolder locker(m_lock);

    COMPtr<IDirect3DSurface9> surface;

    if (sample) {
        COMPtr<IMFMediaBuffer> buffer;
        hr = sample->GetBufferByIndex(0, &buffer);
        hr = MFGetService(buffer.get(), MR_BUFFER_SERVICE, __uuidof(IDirect3DSurface9), (void**)&surface);
    } else if (m_surfaceRepaint) {
        // Use the last surface.
        surface = m_surfaceRepaint;
    }

    if (surface) {
        UINT width = m_destRect.right - m_destRect.left;
        UINT height = m_destRect.bottom - m_destRect.top;

        if (width > 0 && height > 0) {
            if (!m_memSurface || m_width != width || m_height != height) {
                D3DFORMAT format = D3DFMT_A8R8G8B8;
                D3DSURFACE_DESC desc;
                if (SUCCEEDED(surface->GetDesc(&desc)))
                    format = desc.Format;
                m_memSurface.clear();
                hr = m_device->CreateOffscreenPlainSurface(width, height, format, D3DPOOL_SYSTEMMEM, &m_memSurface, nullptr);
                m_width = width;
                m_height = height;
            }
            // Copy data from video memory to system memory
            hr = m_device->GetRenderTargetData(surface.get(), m_memSurface.get());
            if (FAILED(hr)) {
                m_memSurface = nullptr;
                hr = S_OK;
            }
        }

        // Keep the last surface for repaints.
        m_surfaceRepaint = surface;
    }

    if (FAILED(hr)) {
        if (hr == D3DERR_DEVICELOST || hr == D3DERR_DEVICENOTRESET || hr == D3DERR_DEVICEHUNG) {
            // Ignore this error. We have to reset or recreate the device.
            // The presenter will handle this when checking the device state the next time.
            hr = S_OK;
        }
    }
    return hr;
}

void MediaPlayerPrivateMediaFoundation::Direct3DPresenter::paintCurrentFrame(WebCore::GraphicsContext& context, const WebCore::FloatRect& destRect)
{
    UINT width = m_destRect.right - m_destRect.left;
    UINT height = m_destRect.bottom - m_destRect.top;

    if (!width || !height)
        return;

    LockHolder locker(m_lock);

    if (!m_memSurface)
        return;

    D3DLOCKED_RECT lockedRect;
    if (SUCCEEDED(m_memSurface->LockRect(&lockedRect, nullptr, D3DLOCK_READONLY))) {
        void* data = lockedRect.pBits;
        int pitch = lockedRect.Pitch;
#if USE(CAIRO)
        D3DFORMAT format = D3DFMT_UNKNOWN;
        D3DSURFACE_DESC desc;
        if (SUCCEEDED(m_memSurface->GetDesc(&desc)))
            format = desc.Format;

        cairo_format_t cairoFormat = CAIRO_FORMAT_INVALID;

        switch (format) {
        case D3DFMT_A8R8G8B8:
            cairoFormat = CAIRO_FORMAT_ARGB32;
            break;
        case D3DFMT_X8R8G8B8:
            cairoFormat = CAIRO_FORMAT_RGB24;
            break;
        default:
            break;
        }

        ASSERT(cairoFormat != CAIRO_FORMAT_INVALID);

        cairo_surface_t* image = nullptr;
        if (cairoFormat != CAIRO_FORMAT_INVALID)
            image = cairo_image_surface_create_for_data(static_cast<unsigned char*>(data), cairoFormat, width, height, pitch);

        FloatRect srcRect(0, 0, width, height);
        if (image) {
            ASSERT(context.hasPlatformContext());
            auto& state = context.state();
            Cairo::drawSurface(*context.platformContext(), image, destRect, srcRect, state.imageInterpolationQuality, state.alpha, Cairo::ShadowState(state));
            cairo_surface_destroy(image);
        }
#else
#error "Platform needs to implement drawing of Direct3D surface to graphics context!"
#endif
        m_memSurface->UnlockRect();
    }
}

HRESULT MediaPlayerPrivateMediaFoundation::Direct3DPresenter::initializeD3D()
{
    ASSERT(!m_direct3D9);
    ASSERT(!m_deviceManager);

    HRESULT hr = Direct3DCreate9Ex(D3D_SDK_VERSION, &m_direct3D9);
    if (FAILED(hr))
        return hr;

    return DXVA2CreateDirect3DDeviceManager9(&m_deviceResetToken, &m_deviceManager);
}

HRESULT MediaPlayerPrivateMediaFoundation::Direct3DPresenter::createD3DDevice()
{
    HRESULT hr = S_OK;
    UINT adapterID = D3DADAPTER_DEFAULT;

    LockHolder locker(m_lock);

    if (!m_direct3D9 || !m_deviceManager)
        return MF_E_NOT_INITIALIZED;

    HWND hwnd = GetDesktopWindow();

    // We create additional swap chains to present the video frames,
    // and do not use the implicit swap chain of the device.
    // The size of the back buffer is 1 x 1.

    D3DPRESENT_PARAMETERS pp;
    ZeroMemory(&pp, sizeof(pp));

    pp.BackBufferWidth = 1;
    pp.BackBufferHeight = 1;
    pp.Windowed = TRUE;
    pp.SwapEffect = D3DSWAPEFFECT_COPY;
    pp.BackBufferFormat = D3DFMT_UNKNOWN;
    pp.hDeviceWindow = hwnd;
    pp.Flags = D3DPRESENTFLAG_VIDEO;
    pp.PresentationInterval = D3DPRESENT_INTERVAL_DEFAULT;

    if (m_hwnd) {
        HMONITOR monitor = MonitorFromWindow(m_hwnd, MONITOR_DEFAULTTONEAREST);

        hr = findAdapter(m_direct3D9.get(), monitor, adapterID);
        if (FAILED(hr))
            return hr;
    }

    D3DCAPS9 ddCaps;
    ZeroMemory(&ddCaps, sizeof(ddCaps));

    hr = m_direct3D9->GetDeviceCaps(adapterID, D3DDEVTYPE_HAL, &ddCaps);
    if (FAILED(hr))
        return hr;

    DWORD flags = D3DCREATE_NOWINDOWCHANGES | D3DCREATE_MULTITHREADED | D3DCREATE_FPU_PRESERVE;

    if (ddCaps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT)
        flags |= D3DCREATE_HARDWARE_VERTEXPROCESSING;
    else
        flags |= D3DCREATE_SOFTWARE_VERTEXPROCESSING;

    COMPtr<IDirect3DDevice9Ex> device;
    hr = m_direct3D9->CreateDeviceEx(adapterID, D3DDEVTYPE_HAL, pp.hDeviceWindow, flags, &pp, nullptr, &device);
    if (FAILED(hr))
        return hr;

    hr = m_direct3D9->GetAdapterDisplayMode(adapterID, &m_displayMode);
    if (FAILED(hr))
        return hr;

    hr = m_deviceManager->ResetDevice(device.get(), m_deviceResetToken);
    if (FAILED(hr))
        return hr;

    m_device = device;

    return hr;
}

HRESULT MediaPlayerPrivateMediaFoundation::Direct3DPresenter::createD3DSample(IDirect3DSwapChain9* swapChain, COMPtr<IMFSample>& videoSample)
{
    COMPtr<IDirect3DSurface9> surface;
    HRESULT hr = swapChain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &surface);
    if (FAILED(hr))
        return hr;

    D3DCOLOR colorBlack = D3DCOLOR_ARGB(0xFF, 0x00, 0x00, 0x00);
    hr = m_device->ColorFill(surface.get(), nullptr, colorBlack);
    if (FAILED(hr))
        return hr;

    return MFCreateVideoSampleFromSurface(surface.get(), &videoSample);
}

HRESULT MediaPlayerPrivateMediaFoundation::Direct3DPresenter::getSwapChainPresentParameters(IMFMediaType* type, D3DPRESENT_PARAMETERS* presentParams)
{
    if (!m_hwnd)
        return MF_E_INVALIDREQUEST;

    COMPtr<IMFMediaType> videoType = type;

    UINT32 width = 0, height = 0;
    HRESULT hr = MFGetAttributeSize(videoType.get(), MF_MT_FRAME_SIZE, &width, &height);
    if (FAILED(hr))
        return hr;

    GUID guidSubType = GUID_NULL;
    hr = videoType->GetGUID(MF_MT_SUBTYPE, &guidSubType);
    if (FAILED(hr))
        return hr;

    DWORD d3dFormat = guidSubType.Data1;

    ZeroMemory(presentParams, sizeof(D3DPRESENT_PARAMETERS));
    presentParams->BackBufferWidth = width;
    presentParams->BackBufferHeight = height;
    presentParams->Windowed = TRUE;
    presentParams->SwapEffect = D3DSWAPEFFECT_COPY;
    presentParams->BackBufferFormat = (D3DFORMAT)d3dFormat;
    presentParams->hDeviceWindow = m_hwnd;
    presentParams->Flags = D3DPRESENTFLAG_VIDEO;
    presentParams->PresentationInterval = D3DPRESENT_INTERVAL_DEFAULT;

    D3DDEVICE_CREATION_PARAMETERS params;
    hr = m_device->GetCreationParameters(&params);
    if (FAILED(hr))
        return hr;

    if (params.DeviceType != D3DDEVTYPE_HAL)
        presentParams->Flags |= D3DPRESENTFLAG_LOCKABLE_BACKBUFFER;

    return S_OK;
}

} // namespace WebCore

#endif
