/* -*- mode: c; tab-width: 4; c-basic-offset: 4; c-file-style: "linux" -*- */ // // Copyright (c) 2017, SDLPAL development team. // All rights reserved. // // This file is part of SDLPAL. // // SDLPAL is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // // native_avi.cpp: Native Windows Runtime AVI player for SDLPal. // @Author: Lou Yihua, 2017 // #include "pch.h" #include "AsyncHelper.h" #include "StringHelper.h" #include #include #include #include #include #include #include #include "../../3rd/SDL/include/SDL_syswm.h" #include "../../video.h" #include "../../util.h" #include "../../input.h" using namespace Microsoft::WRL; class bstr_t { public: #if WINAPI_FAMILY == WINAPI_FAMILY_PHONE_APP struct _bstr { DWORD length; OLECHAR string[2]; }; bstr_t(const wchar_t* s) : m_bstr(nullptr) { DWORD length = (DWORD)wcslen(s); auto p = (_bstr*)CoTaskMemAlloc(length * sizeof(wchar_t) + sizeof(_bstr)); wcsncpy(m_bstr = p->string, s, length); p->string[length] = p->string[length + 1] = L'\0'; p->length = length * sizeof(wchar_t); } ~bstr_t() { CoTaskMemFree((LPBYTE)m_bstr - offsetof(_bstr, string)); } #else bstr_t(const wchar_t* s) : m_bstr(SysAllocString(s)) {} ~bstr_t() { SysFreeString(m_bstr); } #endif operator BSTR() { return m_bstr; } private: BSTR m_bstr; }; class CUnknown : public IUnknown { public: CUnknown() : m_refcount(1) { } virtual ~CUnknown() { } virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject) { HRESULT hr = S_OK; if (riid == IID_IUnknown) *ppvObject = this, AddRef(); else hr = E_NOINTERFACE; return hr; } virtual ULONG STDMETHODCALLTYPE AddRef(void) { return (ULONG)InterlockedIncrement(&m_refcount); } virtual ULONG STDMETHODCALLTYPE Release(void) { LONG ref = InterlockedDecrement(&m_refcount); if (0 == ref) { delete this; } return ref; } private: volatile LONG m_refcount; }; class CAVIPlayer : public CUnknown , public IMFMediaEngineNotify , public IMFMediaEngineExtension { #define FAIL_RETURN(x) do { HRESULT hr; if (hr = (x)) return hr; } while(0) struct Event { DWORD event; DWORD_PTR param1; DWORD param2; Event(DWORD a, DWORD_PTR b, DWORD c) : event(a), param1(b), param2(c) {} }; class CMFAsyncCallback : public CUnknown , public IMFAsyncCallback { public: CMFAsyncCallback(IMFByteStream* pByteStream, IMFSourceResolver* resolver, IUnknown* punkState, IMFAsyncCallback* pCallback) : m_path(nullptr), m_pByteStream(pByteStream), m_punkState(punkState), m_pCallback(pCallback), m_resolver(resolver) { } CMFAsyncCallback(BSTR bstrURL, IUnknown* punkState, IMFAsyncCallback* pCallback) : m_path(bstrURL ? ref new Platform::String(bstrURL) : nullptr) , m_pByteStream(nullptr), m_punkState(punkState) , m_pCallback(pCallback), m_resolver(nullptr) { } CMFAsyncCallback(BSTR bstrURL, IMFSourceResolver* resolver, IUnknown* punkState, IMFAsyncCallback* pCallback) : m_path(bstrURL ? ref new Platform::String(bstrURL) : nullptr) , m_pByteStream(nullptr), m_punkState(punkState) , m_pCallback(pCallback), m_resolver(resolver) { } virtual ~CMFAsyncCallback() { if (m_path) delete m_path; } virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject) { HRESULT hr = S_OK; if (riid == IID_IMFAsyncCallback) *ppvObject = static_cast(this), AddRef(); else hr = CUnknown::QueryInterface(riid, ppvObject); return hr; } virtual ULONG STDMETHODCALLTYPE AddRef(void) { return CUnknown::AddRef(); } virtual ULONG STDMETHODCALLTYPE Release(void) { return CUnknown::Release(); } virtual HRESULT STDMETHODCALLTYPE GetParameters( __RPC__out DWORD *pdwFlags, __RPC__out DWORD *pdwQueue) { return E_NOTIMPL; } virtual HRESULT STDMETHODCALLTYPE Invoke(__RPC__in_opt IMFAsyncResult *pAsyncResult) { HRESULT hr; if (m_path != nullptr) { hr = CreateMFByteStreamFromURL(m_path, m_pByteStream.ReleaseAndGetAddressOf()); if (m_resolver.Get() == nullptr) { return InvokeCallback(m_pByteStream.Get(), hr); } } ComPtr result; MF_OBJECT_TYPE type; hr = m_resolver->CreateObjectFromByteStream(m_pByteStream.Get(), nullptr, MF_RESOLUTION_MEDIASOURCE | MF_RESOLUTION_READ, nullptr, &type, result.GetAddressOf()); return InvokeCallback(result.Get(), hr); } protected: HRESULT InvokeCallback(IUnknown* pObject, HRESULT hr) { ComPtr pResult; HRESULT nhr = MFCreateAsyncResult(pObject, m_pCallback.Get(), m_punkState.Get(), pResult.GetAddressOf()); if (SUCCEEDED(nhr)) { pResult->SetStatus(hr); nhr = MFInvokeCallback(pResult.Get()); } return nhr; } static HRESULT CreateMFByteStreamFromURL(Platform::String^ path, IMFByteStream** ppByteStream) { HRESULT hr; if (!ppByteStream) return E_POINTER; try { auto file = AWait(Windows::Storage::StorageFile::GetFileFromPathAsync(path)); auto stream = AWait(file->OpenAsync(Windows::Storage::FileAccessMode::Read)); hr = MFCreateMFByteStreamOnStreamEx(reinterpret_cast(stream), ppByteStream); delete stream; delete file; } catch (Exception ^e) { hr = e->HResult; } return hr; } private: Platform::String^ m_path; ComPtr m_pByteStream; ComPtr m_punkState; ComPtr m_pCallback; ComPtr m_resolver; }; public: CAVIPlayer() : m_event(CreateEventEx(nullptr, nullptr, 0, EVENT_ALL_ACCESS)) , m_playing(false) { InitializeCriticalSectionEx(&m_cs, 4000, 0); } ~CAVIPlayer() { DeleteCriticalSection(&m_cs); } HRESULT Initialize() { ComPtr mtctx; ComPtr mfmecf; ComPtr mfattr; static const D3D_FEATURE_LEVEL featureLevels[] = { D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0, D3D_FEATURE_LEVEL_9_3, D3D_FEATURE_LEVEL_9_1 }; FAIL_RETURN(D3D11CreateDevice( nullptr, // specify nullptr to use the default adapter D3D_DRIVER_TYPE_HARDWARE, nullptr, // leave as nullptr if hardware is used D3D11_CREATE_DEVICE_VIDEO_SUPPORT | D3D11_CREATE_DEVICE_BGRA_SUPPORT, featureLevels, ARRAYSIZE(featureLevels), D3D11_SDK_VERSION, // always set this to D3D11_SDK_VERSION m_device.ReleaseAndGetAddressOf(), nullptr, m_context.ReleaseAndGetAddressOf() )); FAIL_RETURN(m_context.As(&mtctx)); FAIL_RETURN(mtctx->SetMultithreadProtected(TRUE)); UINT resetToken; FAIL_RETURN(MFCreateDXGIDeviceManager(&resetToken, m_manager.ReleaseAndGetAddressOf())); FAIL_RETURN(m_manager->ResetDevice(m_device.Get(), resetToken)); FAIL_RETURN(CoCreateInstance(CLSID_MFMediaEngineClassFactory, nullptr, CLSCTX_ALL, IID_PPV_ARGS(mfmecf.GetAddressOf()))); FAIL_RETURN(MFCreateAttributes(mfattr.GetAddressOf(), 1)); FAIL_RETURN(mfattr->SetUnknown(MF_MEDIA_ENGINE_CALLBACK, static_cast(this))); FAIL_RETURN(mfattr->SetUnknown(MF_MEDIA_ENGINE_EXTENSION, static_cast(this))); FAIL_RETURN(mfattr->SetUnknown(MF_MEDIA_ENGINE_DXGI_MANAGER, m_manager.Get())); FAIL_RETURN(mfattr->SetUINT32(MF_MEDIA_ENGINE_VIDEO_OUTPUT_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM)); FAIL_RETURN(mfmecf->CreateInstance(0, mfattr.Get(), m_engine.ReleaseAndGetAddressOf())); FAIL_RETURN(MFCreateSourceResolver(m_resolver.GetAddressOf())); m_playing = false; ResetEvent(m_event); m_events.clear(); return S_OK; } //HRESULT HRESULT Load(Platform::String^ filePath, DWORD* cx, DWORD* cy) { if (!filePath || !cx || !cy) return E_POINTER; if (m_engine) { ResetEvent(m_event); FAIL_RETURN(m_engine->SetSource(bstr_t(filePath->Data()))); FAIL_RETURN(m_engine->Load()); bool abort = false, ready = false; while (WaitForSingleObjectEx(m_event, INFINITE, FALSE) == WAIT_OBJECT_0 && !abort && !ready) { EnterCriticalSection(&m_cs); for (auto i = m_events.begin(); i != m_events.end(); i = m_events.erase(i)) { switch (i->event) { case MF_MEDIA_ENGINE_EVENT_ERROR: UTIL_LogOutput(LOGLEVEL_ERROR, "Native AVI player encountered error: %d (%08x)!", i->param1, i->param2); break; case MF_MEDIA_ENGINE_EVENT_ABORT: abort = true; break; case MF_MEDIA_ENGINE_EVENT_CANPLAY: ready = true; break; } } LeaveCriticalSection(&m_cs); } if (abort || !ready) return E_FAIL; FAIL_RETURN(hr = m_engine->GetNativeVideoSize(cx, cy)); D3D11_TEXTURE2D_DESC desc = { *cx, *cy, 1, 1, DXGI_FORMAT_B8G8R8A8_UNORM, { 1, 0 }, D3D11_USAGE_DEFAULT, D3D11_BIND_RENDER_TARGET, 0, 0 }; FAIL_RETURN(m_device->CreateTexture2D(&desc, nullptr, m_render.ReleaseAndGetAddressOf())); desc.Usage = D3D11_USAGE_STAGING; desc.BindFlags = 0; desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; FAIL_RETURN(m_device->CreateTexture2D(&desc, nullptr, m_reader.ReleaseAndGetAddressOf())); return S_OK; } return E_FAIL; } template HRESULT Play(_Callback callback) { HRESULT hr = E_FAIL; if (m_engine && SUCCEEDED(hr = m_engine->Play())) { MFVideoNormalizedRect rect = { 0.0f, 0.0f, 1.0f, 1.0f }; RECT rcTarget = { 0 }; MFARGB bkgColor = { 0 }; bool playing = true, skip = false; m_engine->GetNativeVideoSize((DWORD*)&rcTarget.right, (DWORD*)&rcTarget.bottom); while (playing && !skip) { LONGLONG pts; if (m_engine->OnVideoStreamTick(&pts) == S_OK && m_engine->TransferVideoFrame(m_render.Get(), &rect, &rcTarget, &bkgColor) == S_OK) { D3D11_MAPPED_SUBRESOURCE res; // new frame available at the media engine so get it m_context->CopyResource(m_reader.Get(), m_render.Get()); m_context->Flush(); m_context->Map(m_reader.Get(), 0, D3D11_MAP_READ, 0, &res); skip = callback(res.pData, res.RowPitch); m_context->Unmap(m_reader.Get(), 0); } if (WaitForSingleObjectEx(m_event, 0, FALSE) == WAIT_OBJECT_0) { EnterCriticalSection(&m_cs); for (auto i = m_events.begin(); i != m_events.end(); i = m_events.erase(i)) { if (i->event == MF_MEDIA_ENGINE_EVENT_ENDED) { playing = false; break; } } LeaveCriticalSection(&m_cs); } } hr = skip ? S_FALSE : S_OK; } return hr; } HRESULT Pause() { if (m_engine) { HRESULT hr = m_engine->Pause(); if (m_playing && SUCCEEDED(hr)) m_playing = false; return hr; } else { m_playing = false; return E_NOT_VALID_STATE; } } HRESULT Shutdown() { if (m_engine) { HRESULT hr = m_engine->Shutdown(); if (m_playing && SUCCEEDED(hr)) m_playing = false; return hr; } else { m_playing = false; return E_NOT_VALID_STATE; } } bool IsPlaying() const { return m_playing; } // *************************************************************************************** // IUnknown // *************************************************************************************** virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject) { HRESULT hr = S_OK; if (!ppvObject) return E_POINTER; if (riid == IID_IMFMediaEngineNotify) *ppvObject = static_cast(this); else if (riid == IID_IMFMediaEngineExtension) *ppvObject = static_cast(this); else hr = CUnknown::QueryInterface(riid, ppvObject); return hr; } virtual ULONG STDMETHODCALLTYPE AddRef(void) { return 1; } virtual ULONG STDMETHODCALLTYPE Release(void) { return 1; } // *************************************************************************************** // IMFMediaEngineNotify // *************************************************************************************** virtual HRESULT STDMETHODCALLTYPE EventNotify(DWORD event, DWORD_PTR param1, DWORD param2) { EnterCriticalSection(&m_cs); if (event == MF_MEDIA_ENGINE_EVENT_ERROR) { ComPtr error; m_engine->GetError(&error); m_events.push_back(Event(event, error->GetErrorCode(), error->GetExtendedErrorCode())); } else { m_events.push_back(Event(event, param1, param2)); } LeaveCriticalSection(&m_cs); SetEvent(m_event); return S_OK; } // *************************************************************************************** // IMFMediaEngineExtension // *************************************************************************************** virtual HRESULT STDMETHODCALLTYPE CanPlayType( _In_ BOOL AudioOnly, _In_ BSTR MimeType, _Out_ MF_MEDIA_ENGINE_CANPLAY *pAnswer) { if (!pAnswer) return E_POINTER; if (_wcsicmp(MimeType, L"video/avi") == 0) *pAnswer = MF_MEDIA_ENGINE_CANPLAY_PROBABLY; else *pAnswer = MF_MEDIA_ENGINE_CANPLAY_NOT_SUPPORTED; return S_OK; } virtual HRESULT STDMETHODCALLTYPE BeginCreateObject( _In_ BSTR bstrURL, _In_opt_ IMFByteStream *pByteStream, _In_ MF_OBJECT_TYPE type, _Outptr_ IUnknown **ppIUnknownCancelCookie, _In_ IMFAsyncCallback *pCallback, _In_opt_ IUnknown *punkState) { if (!pCallback) return E_POINTER; switch (type) { case MF_OBJECT_BYTESTREAM: if (!bstrURL) return E_POINTER; break; case MF_OBJECT_MEDIASOURCE: if (!bstrURL && !pByteStream) return E_POINTER; break; default: return E_INVALIDARG; } ComPtr pcb; if (type == MF_OBJECT_BYTESTREAM) { if (!pByteStream) { pcb.Attach(new CMFAsyncCallback(bstrURL, punkState, pCallback)); } else { return E_INVALIDARG; } } if (type == MF_OBJECT_MEDIASOURCE) { if (!pByteStream) { pcb.Attach(new CMFAsyncCallback(bstrURL, m_resolver.Get(), punkState, pCallback)); } else { pcb.Attach(new CMFAsyncCallback(pByteStream, m_resolver.Get(), punkState, pCallback)); } } return MFPutWorkItem2(MFASYNC_CALLBACK_QUEUE_MULTITHREADED, 0, pcb.Get(), nullptr); } virtual HRESULT STDMETHODCALLTYPE CancelObjectCreation( _In_ IUnknown *pIUnknownCancelCookie) { return S_OK; } virtual HRESULT STDMETHODCALLTYPE EndCreateObject( _In_ IMFAsyncResult *pResult, _Outptr_ IUnknown **ppObject) { return pResult->GetObject(ppObject); } private: HANDLE m_event; CRITICAL_SECTION m_cs; ComPtr m_device; ComPtr m_context; ComPtr m_manager; ComPtr m_engine; ComPtr m_resolver; ComPtr m_render, m_reader; std::list m_events; bool m_playing; #undef FAIL_RETURN }; extern "C" BOOL PAL_PlayAVI_Native(const char *lpszPath) { CAVIPlayer player; HRESULT hr; DWORD cx, cy; if (FAILED(hr = player.Initialize())) return FALSE; if (FAILED(hr = player.Load(ConvertString(lpszPath), &cx, &cy))) return FALSE; PAL_ClearKeyState(); SDL_Surface* surface = SDL_CreateRGBSurface(SDL_SWSURFACE, cx, cy, 32, 0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000); hr = player.Play([surface](const void* data, UINT pitch)->bool { if (pitch == surface->pitch) { memcpy(surface->pixels, data, surface->pitch * surface->h); } else { auto dst = (uint8_t*)surface->pixels - surface->pitch, src = (uint8_t*)data - pitch; auto length = surface->w << 2; for (int i = 0; i < surface->h; i++, memcpy(dst += surface->pitch, src += pitch, length)); } VIDEO_DrawSurfaceToScreen(surface); UTIL_Delay(1); return (g_InputState.dwKeyPress & (kKeyMenu | kKeySearch)) != 0; }); SDL_FreeSurface(surface); if (FAILED(hr)) return FALSE; if (hr == S_FALSE) { // // Simulate a short delay (like the original game) // UTIL_Delay(500); } hr = player.Pause(); hr = player.Shutdown(); return TRUE; }