From eaff36944c8b66dcf10ae504aa42887263692120 Mon Sep 17 00:00:00 2001 From: Sir Walrus Date: Mon, 31 Jul 2023 20:15:15 +0200 Subject: [PATCH] Initial commit --- .gitignore | 12 + Makefile | 130 + README.md | 79 + fake-krmovie.cpp | 700 +++++ gstkrkr.c | 473 +++ install.py | 209 ++ krkrwine.cpp | 2326 ++++++++++++++ making.md | 722 +++++ making/closeenough.png | Bin 0 -> 9677 bytes making/finally.png | Bin 0 -> 126716 bytes making/segfault.png | Bin 0 -> 12278 bytes pl_mpeg.h | 4265 ++++++++++++++++++++++++++ runner.cpp | 253 ++ x/d3d9-on-child-window.cpp | 72 + x/gstkrkr-demux-video.c | 722 +++++ x/gstkrkr-video-audio-fakewma.c | 648 ++++ x/homemade-source-and-sink.cpp | 829 +++++ x/messy-runner-custom-vmr9.cpp | 660 ++++ x/nanodecode-with-krkr-weirdness.cpp | 923 ++++++ x/nanodecode.cpp | 248 ++ x/plmpeg_wine_dmo.cpp | 784 +++++ 21 files changed, 14055 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 fake-krmovie.cpp create mode 100644 gstkrkr.c create mode 100755 install.py create mode 100644 krkrwine.cpp create mode 100644 making.md create mode 100644 making/closeenough.png create mode 100644 making/finally.png create mode 100644 making/segfault.png create mode 100644 pl_mpeg.h create mode 100644 runner.cpp create mode 100644 x/d3d9-on-child-window.cpp create mode 100644 x/gstkrkr-demux-video.c create mode 100644 x/gstkrkr-video-audio-fakewma.c create mode 100644 x/homemade-source-and-sink.cpp create mode 100644 x/messy-runner-custom-vmr9.cpp create mode 100644 x/nanodecode-with-krkr-weirdness.cpp create mode 100644 x/nanodecode.cpp create mode 100644 x/plmpeg_wine_dmo.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..77e51e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +Glorious-Eggroll/ +mk/ +gstkrkr-i386.so +gstkrkr-x86_64.so +krkrwine-i386.dll +krkrwine-x86_64.dll +run32.exe +run64.exe +*.wmv +*.WMV +*.mpg +*.mpeg diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5089ec5 --- /dev/null +++ b/Makefile @@ -0,0 +1,130 @@ +# SPDX-License-Identifier: LGPL-2.0-or-later + +FLAGS := -Wall +ifeq ($(OPT),1) + FLAGS := -Os -s -g0 -fno-rtti -Wall +endif + +all: gstkrkr-x86_64.so gstkrkr-i386.so krkrwine-x86_64.dll krkrwine-i386.dll + +clean: + -rm gstkrkr-x86_64.so gstkrkr-i386.so krkrwine-x86_64.dll krkrwine-i386.dll + -rm run32.exe run64.exe krmovie.dll + +MINGW32 = i686-w64-mingw32-g++-win32 -std=c++20 -fno-exceptions -gdwarf-4 -static-libgcc -static-libstdc++ $(FLAGS) +MINGW64 = x86_64-w64-mingw32-g++-win32 -std=c++20 -fno-exceptions -gdwarf-4 -static-libgcc -static-libstdc++ $(FLAGS) + +gstkrkr-x86_64.so: gstkrkr.c Makefile + gcc gstkrkr.c -shared -fPIC -std=c99 -DPLUGINARCH=x86_64 -o gstkrkr-x86_64.so -g $(shell pkg-config --cflags --libs gstreamer-1.0) -fvisibility=hidden -Wl,--no-undefined $(FLAGS) + +# must use -std=c##, the default (gnu##) predefines the symbol i386 +gstkrkr-i386.so: gstkrkr.c Makefile + gcc -m32 gstkrkr.c -shared -fPIC -std=c99 -DPLUGINARCH=i386 -o gstkrkr-i386.so $(shell pkg-config --personality=i386-linux-gnu --cflags --libs gstreamer-1.0) -fvisibility=hidden -Wl,--no-undefined $(FLAGS) + +krkrwine-x86_64.dll: krkrwine.cpp Makefile + $(MINGW64) krkrwine.cpp -shared -fPIC -o krkrwine-x86_64.dll -lole32 + +krkrwine-i386.dll: krkrwine.cpp Makefile + $(MINGW32) krkrwine.cpp -shared -fPIC -o krkrwine-i386.dll -lole32 + +prepare: + cp $(EGGROLL)/files/lib64/gstreamer-1.0/libgstmpegpsdemux.so Glorious-Eggroll/x86_64-libgstmpegpsdemux.so + cp $(EGGROLL)/files/lib64/gstreamer-1.0/libgstasf.so Glorious-Eggroll/x86_64-libgstasf.so + cp $(EGGROLL)/files/lib64/libgstcodecparsers-1.0.so.0 Glorious-Eggroll/x86_64-libgstcodecparsers-1.0.so.0 + cp $(EGGROLL)/files/lib64/libavcodec.so.58 Glorious-Eggroll/x86_64-libavcodec.so.58 + cp $(EGGROLL)/files/lib64/libavutil.so.56 Glorious-Eggroll/x86_64-libavutil.so.56 + cp $(EGGROLL)/files/lib64/libavfilter.so.7 Glorious-Eggroll/x86_64-libavfilter.so.7 + cp $(EGGROLL)/files/lib64/libavformat.so.58 Glorious-Eggroll/x86_64-libavformat.so.58 + cp $(EGGROLL)/files/lib64/libavdevice.so.58 Glorious-Eggroll/x86_64-libavdevice.so.58 + cp $(EGGROLL)/files/lib64/libswresample.so.3 Glorious-Eggroll/x86_64-libswresample.so.3 + cp $(EGGROLL)/files/lib64/libswscale.so.5 Glorious-Eggroll/x86_64-libswscale.so.5 + cp $(EGGROLL)/files/lib/gstreamer-1.0/libgstmpegpsdemux.so Glorious-Eggroll/i386-libgstmpegpsdemux.so + cp $(EGGROLL)/files/lib/gstreamer-1.0/libgstasf.so Glorious-Eggroll/i386-libgstasf.so + cp $(EGGROLL)/files/lib/libgstcodecparsers-1.0.so.0 Glorious-Eggroll/i386-libgstcodecparsers-1.0.so.0 + cp $(EGGROLL)/files/lib/libavcodec.so.58 Glorious-Eggroll/i386-libavcodec.so.58 + cp $(EGGROLL)/files/lib/libavutil.so.56 Glorious-Eggroll/i386-libavutil.so.56 + cp $(EGGROLL)/files/lib/libavfilter.so.7 Glorious-Eggroll/i386-libavfilter.so.7 + cp $(EGGROLL)/files/lib/libavformat.so.58 Glorious-Eggroll/i386-libavformat.so.58 + cp $(EGGROLL)/files/lib/libavdevice.so.58 Glorious-Eggroll/i386-libavdevice.so.58 + cp $(EGGROLL)/files/lib/libswresample.so.3 Glorious-Eggroll/i386-libswresample.so.3 + cp $(EGGROLL)/files/lib/libswscale.so.5 Glorious-Eggroll/i386-libswscale.so.5 + + +krkrwine.tar.gz: | Glorious-Eggroll/x86_64-libgstmpegpsdemux.so + $(MAKE) clean + $(MAKE) OPT=1 + -rm krkrwine.tar.gz + tar -czvf krkrwine.tar.gz --transform='s%^%krkrwine/%' --show-transformed-names README.md install.py gstkrkr-i386.so gstkrkr-x86_64.so krkrwine-i386.dll krkrwine-x86_64.dll Glorious-Eggroll/ + + +# The following is various debug tools. If you're just trying to compile krkrwine, you don't need them; +# if you're trying to debug or patch this program, you may find some of them useful. (Some of them are hardcoded for my own use.) + +run32.exe: runner.cpp Makefile + $(MINGW32) runner.cpp -o run32.exe -lole32 -ld3d9 + +run64.exe: runner.cpp Makefile + $(MINGW64) runner.cpp -o run64.exe -lole32 -ld3d9 -lgdi32 + +run: krkrwine-x86_64.dll gstkrkr-x86_64.so run64.exe + GST_DEBUG_NO_COLOR=1 WINEDEBUG=+warn,+error,+fixme timeout 10 wine run64.exe 2>&1 | guidfilt + +run32: krkrwine-i386.dll gstkrkr-i386.so run32.exe + GST_DEBUG_NO_COLOR=1 timeout 10 wine run32.exe 2>&1 | guidfilt + +runv: krkrwine-x86_64.dll gstkrkr-x86_64.so run64.exe + GST_DEBUG_NO_COLOR=1 GST_DEBUG=6 WINEDEBUG=trace+quartz,warn+quartz timeout 10 wine run64.exe 2>&1 | guidfilt | head -n2000 | tee e.log + +mk/microkiri.exe: + echo 'Download and extract microkiri from https://bugs.winehq.org/show_bug.cgi?id=9127#c102 to the mk/ subdirectory' + mkdir mk + false +mk/_rmovie.dll: mk/microkiri.exe + mv mk/krmovie.dll mk/_rmovie.dll +krmovie.dll: fake-krmovie.cpp Makefile + $(MINGW32) fake-krmovie.cpp -shared -fPIC -o krmovie.dll -lole32 -ld3d9 +rmk: krmovie.dll krkrwine-i386.dll mk/_rmovie.dll + -rm mk/krmovie.dll + cp krmovie.dll mk/krmovie.dll + LC_ALL=ja_JP wine mk/microkiri.exe + +rmkd: krmovie.dll krkrwine-i386.dll mk/_rmovie.dll + -rm mk/krmovie.dll + cp krmovie.dll mk/krmovie.dll + LC_ALL=ja_JP wine ~/tools/mingw64-11.2.0/bin/gdb.exe -ex "set disassemble-next-line on" -ex "set disassembly-flavor intel" mk/microkiri.exe + +rwa: krkrwine-i386.dll gstkrkr-i386.so krmovie.dll + LC_ALL=ja_JP wine /games/wine/waga/waga/waga.exe +rwas: krkrwine-i386.dll gstkrkr-i386.so krmovie.dll + LC_ALL=ja_JP timeout 7 wine /games/wine/waga/waga/waga.exe + +wau: + mv /games/wine/waga/waga/plugin/_rmovie.dll /games/wine/waga/waga/plugin/krmovie.dll +wai: + mv -n /games/wine/waga/waga/plugin/krmovie.dll /games/wine/waga/waga/plugin/_rmovie.dll + ln -s $(shell realpath .)/krmovie.dll /games/wine/waga/waga/plugin/krmovie.dll + +X1_HOME = /home/walrus/mount/x1/home/x1/ +X1_PROTON = $(X1_HOME)steam/steamapps/common/"Proton 8.0"/ +X1_PFX = $(X1_PROTON)dist/share/default_pfx/ +X1_DLL = $(X1_PFX)drive_c/windows/syswow64/krkrwine.dll +X1_EXE = $(X1_HOME)Desktop/a.exe +X1_GST = $(X1_PROTON)dist/lib/gstreamer-1.0/gstkrkr-i386.so +ax1: run32.exe krkrwine-i386.dll gstkrkr-i386.so + -rm $(X1_DLL) $(X1_EXE) $(X1_GST) + cp krkrwine-i386.dll $(X1_DLL) + cp run32.exe $(X1_EXE) + cp gstkrkr-i386.so $(X1_GST) + +installx1: all + ./install.py $(X1_PROTON) + +installx2: all + ./install.py /home/walrus/mount/x2/home/x2/steam/steamapps/common/"Proton 8.0"/ + +X2_WAGAPATH = /home/walrus/mount/x2/home/x2/steam/steamapps/common/"WAGAMAMA HIGH SPEC" +waux2: + mv $(X2_WAGAPATH)/plugin/_rmovie.dll $(X2_WAGAPATH)/plugin/krmovie.dll +waix2: krmovie.dll + mv -n $(X2_WAGAPATH)/plugin/krmovie.dll $(X2_WAGAPATH)/plugin/_rmovie.dll + cp krmovie.dll $(X2_WAGAPATH)/plugin/krmovie.dll diff --git a/README.md b/README.md new file mode 100644 index 0000000..23d69e5 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +krkrwine +======== + +This program implements various missing functionality in Wine / Proton, such that videos work properly in Kirikiri-based visual novels. (Other games are not targetted by krkrwine, but they may be improved as well.) + +Installation - Linux/Proton +--------------------------- + +Simply run ./install.py ~/steam/steamapps/common/Proton\ 8.0/, and it will install into every current and future game using that Proton. Installing krkrwine into Glorious Eggroll should work too, though this is untested. + +krkrwine does not replace any existing files, and Steam's updater ignores files it doesn't recognize; therefore, you don't need to reinstall krkrwine if Proton updates. (However, if you reinstall Proton, or download a new Proton version, you obviously need to reinstall krkrwine too.) + +krkrwine is only tested in Proton 8.0; I don't think it'll break too hard in anything else, but it's untested. + +Installation - Linux/Wine +------------------------- + +On Debian, + +- sudo apt install gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-plugins-good:i386 gstreamer1.0-plugins-bad:i386 gstreamer1.0-plugins-ugly:i386 gstreamer1.0-libav:i386 +- ./install.py --wine + +On other distros, the package names are different. + +Note that on Debian, gstreamer1.0-libav is not part of, or a dependency of, gstreamer1.0-plugins-good, -bad, nor -ugly; it must be installed separately. I haven't checked other distros. + +install.py will obey the WINEPREFIX environment variable, if set. + +gstkrkr will not be installed; it does nothing useful if you have the above plugins and don't have protonmediaconverter. If you want them anyways for development purposes, you can copy or link them to ~/.local/share/gstreamer-1.0/plugins/. + +Installation - macOS +-------------------- + +See the Linux/Wine steps, it'll probably work. However, it's untested, and I can't help you if it breaks. + +Installation - Windows +---------------------- + +Don't. All relevant functionality is already in place on every Windows edition I'm aware of, and some parts of this program depend on Wine internals that are different on Windows. + +Compilation +----------- + +- Download and extract your favorite release of Glorious Eggroll (I use 8.4; no real reason, I just picked one) +- sudo apt install make gcc libgstreamer1.0-dev gcc-multilib libgstreamer1.0-dev:i386 g++-mingw-w64-i686-win32 g++-mingw-w64-x86-64-win32 +- make prepare EGGROLL=/home/user/steam/compatibilitytools.d/GE-Proton8-4/ +- make +- ./install.py /home/user/steam/steamapps/common/Proton\ 8.0/ +If desired, you may acquire i686-w64-mingw32-g++-win32 and x86_64-w64-mingw32-g++-win32 programs from elsewhere. I use https://winlibs.com/ in Wine. + +License +------- + +krkrwine itself is LGPL-2.0, same as Wine and GStreamer. Some test programs are GPL-2.0, since they contain code copied from Kirikiri. + +The Glorious-Eggroll subdirectory of the releases is, as the name implies, copied from a Glorious Eggroll release. They're either LGPL-2.0 or GPL-2.0, depending on how they were compiled; I didn't check. + +Upstreaming +----------- + +Ideally, this project would be unnecessary. As such, I'm offering bounties on fixing the Windows-side issues in upstream Wine. + +- $250 - CLSID_MPEG1Splitter video output, and CLSID_CMpegVideoCodec (these objects may use GStreamer, of course) +- $25 - CLSID_MPEG1Splitter IAMStreamSelect (doesn't need to be fully implemented, just needs the parts Kirikiri uses) +- $25 - CLSID_VideoMixingRenderer9 ChangeD3DDevice and NotifyEvent +- $500 - WMCreateSyncReader compressed output, CLSID_CWMADecMediaObject, and CLSID_CWMVDecMediaObject +- $100 - Direct3D 9 on WS_CHILD windows under wined3d +- $25 - make WMSyncReader resize its allocator, so it can output RGB32 properly +- $100 - figure out what's going on with the memory allocator and VFW_E_NOT_COMMITTED, and solve it +- $0 - anything involving gstkrkr and Proton's GStreamer. That's a patent issue; it's a question for lawyers, not programmers. It's only needed in Proton, not vanilla Wine. +- Anything that Kirikiri needs but isn't in the above list - that's a bug in this readme, contact me + +I can't do it myself; I've debugged some things too hard, and have single-stepped and disassembled Windows DLLs. + +You are allowed, but not required, to base such efforts on this project. My architecture is very different from Wine's existing objects, and many of them are implemented in an awful way, so most of my code is unusable; but you're welcome to look for hints on the objects' expected behavior, or otherwise use them to help implement yours. You may also use the Kirikiri source code, + +To claim a bounty, post at , or email me at sir@walrus.se. These bounties can be combined with similar offers made by others. + +If I see any of the above implemented, but nobody claims the bounty, I will donate it to Wine. diff --git a/fake-krmovie.cpp b/fake-krmovie.cpp new file mode 100644 index 0000000..b8138ff --- /dev/null +++ b/fake-krmovie.cpp @@ -0,0 +1,700 @@ +// SPDX-License-Identifier: GPL-2.0-only +// This file contains plenty of headers and function prototypes copypasted from Kirikiri . +// Kirikiri is dual licensed under GPLv2 and some homemade license. Since the homemade license is Japanese, +// and I can't read that, I can't accept that license; therefore, this file is GPLv2 only. +// (I'm not sure if headers and other required interopability data is covered by copyright, but better safe than sorry.) +// (I also don't know if Kirikiri is 2.0 only or 2.0 or later, so I'll pick the safe choice.) + +//#define DOIT + +#define STRSAFE_NO_DEPRECATE +#define INITGUID +#include +#include +#include +#include +#include +#include + +#ifdef __i386__ +// needs some extra shenanigans to kill the stdcall @12 suffix +#define EXPORT_STDCALL(ret, name, args) \ + __asm__(".section .drectve; .ascii \" -export:" #name "\"; .text"); \ + extern "C" __stdcall ret name args __asm__("_" #name); \ + extern "C" __stdcall ret name args +#else +#define EXPORT_STDCALL(ret, name, args) \ + extern "C" __attribute__((__visibility__("default"))) __stdcall ret name args; \ + extern "C" __attribute__((__visibility__("default"))) __stdcall ret name args +#endif + +static char* guid_to_str(const GUID& guid) +{ + static char buf[8][64]; + static int n = 0; + char* ret = buf[n++%8]; + sprintf(ret, "{%08lX-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}", guid.Data1, guid.Data2, guid.Data3, guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3], guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]); + return ret; +} + +void* pe_get_section_body(HMODULE mod, int sec) +{ + uint8_t* base_addr = (uint8_t*)mod; + IMAGE_DOS_HEADER* head_dos = (IMAGE_DOS_HEADER*)base_addr; + IMAGE_NT_HEADERS* head_nt = (IMAGE_NT_HEADERS*)(base_addr + head_dos->e_lfanew); + IMAGE_DATA_DIRECTORY section_dir = head_nt->OptionalHeader.DataDirectory[sec]; + return base_addr + section_dir.VirtualAddress; +} + +typedef void(*fptr)(); +void override_imports(HMODULE mod, fptr(*override_import)(const char * name)) +{ + uint8_t* base_addr = (uint8_t*)mod; + IMAGE_IMPORT_DESCRIPTOR* imports = (IMAGE_IMPORT_DESCRIPTOR*)pe_get_section_body(mod, IMAGE_DIRECTORY_ENTRY_IMPORT); + + while (imports->Name) + { + //const char * libname = (char*)(base_addr + imports->Name); + //HMODULE mod_src = GetModuleHandle(libname); +//say(libname); +//say("\n"); + + void* * out = (void**)(base_addr + imports->FirstThunk); + // scan the library's exports and see what this one is named + // if OriginalFirstThunk, the names are still available, but FirstThunk-only has to work anyways, no point not using it everywhere + + while (*out) + { + // forwarder RVAs mean the imported function may end up pointing to a completely different dll + // for example, kernel32!HeapAlloc is actually ntdll!RtlAllocateHeap + HMODULE mod_src; + GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (char*)*out, &mod_src); + + uint8_t* src_base_addr = (uint8_t*)mod_src; + IMAGE_EXPORT_DIRECTORY* exports = (IMAGE_EXPORT_DIRECTORY*)pe_get_section_body(mod_src, IMAGE_DIRECTORY_ENTRY_EXPORT); + + DWORD * addr_off = (DWORD*)(src_base_addr + exports->AddressOfFunctions); + DWORD * name_off = (DWORD*)(src_base_addr + exports->AddressOfNames); + WORD * ordinal = (WORD*)(src_base_addr + exports->AddressOfNameOrdinals); + for (size_t i=0;iNumberOfFunctions;i++) + { + if (src_base_addr+addr_off[i] == (uint8_t*)*out) + { + for (size_t j=0;jNumberOfNames;j++) + { + if (ordinal[j] == i) + { + fptr newptr = override_import((const char*)(src_base_addr + name_off[j])); + if (newptr) + { + // can't just *out = newptr, it'll blow up if the import table is in .rdata or otherwise readonly + WriteProcessMemory(GetCurrentProcess(), out, &newptr, sizeof(newptr), NULL); + } + goto done; + } + } +puts("imported a nameless function\n"); +goto done; + } + } +puts("imported a non-exported function\n"); + done: + + //for (size_t i=0;iNumberOfNames;i++) + //{ + // const char * exp_name = (const char*)(base_addr + name_off[i]); + // if (streq(name, exp_name)) + // return base_addr + addr_off[ordinal[i]]; + //} + + out++; + } + imports++; + } +} + + + + +typedef int8_t tjs_int8; +typedef uint8_t tjs_uint8; +typedef int16_t tjs_int16; +typedef uint16_t tjs_uint16; +typedef int32_t tjs_int32; +typedef uint32_t tjs_uint32; +typedef int64_t tjs_int64; +typedef uint64_t tjs_uint64; +typedef int tjs_int; /* at least 32bits */ +typedef unsigned int tjs_uint; /* at least 32bits */ +#define TJS_VS_SHORT_LEN 21 + +typedef wchar_t tjs_char; +typedef unsigned int tjs_uint; +#ifndef TJS_INTF_METHOD +#define TJS_INTF_METHOD __cdecl +#endif + +enum tTVPVideoStatus { vsStopped, vsPlaying, vsPaused, vsProcessing }; + +struct iTVPFunctionExporter +{ + virtual bool TJS_INTF_METHOD QueryFunctions(const tjs_char ** name, void ** function, tjs_uint count) = 0; + virtual bool TJS_INTF_METHOD QueryFunctionsByNarrowString(const char ** name, void ** function, tjs_uint count) = 0; +}; + +class iTVPVideoOverlay // this is not a COM object +{ +public: + virtual void __stdcall AddRef() = 0; + virtual void __stdcall Release() = 0; + + virtual void __stdcall SetWindow(HWND window) = 0; + virtual void __stdcall SetMessageDrainWindow(HWND window) = 0; + virtual void __stdcall SetRect(RECT *rect) = 0; + virtual void __stdcall SetVisible(bool b) = 0; + virtual void __stdcall Play() = 0; + virtual void __stdcall Stop() = 0; + virtual void __stdcall Pause() = 0; + virtual void __stdcall SetPosition(uint64_t tick) = 0; + virtual void __stdcall GetPosition(uint64_t *tick) = 0; + virtual void __stdcall GetStatus(tTVPVideoStatus* status) = 0; + virtual void __stdcall GetEvent(long* evcode, long* param1, long* param2, bool* got) = 0; + +// Start: Add: T.Imoto + virtual void __stdcall FreeEventParams(long evcode, long param1, long param2) = 0; + + virtual void __stdcall Rewind() = 0; + virtual void __stdcall SetFrame( int f ) = 0; + virtual void __stdcall GetFrame( int* f ) = 0; + virtual void __stdcall GetFPS( double* f ) = 0; + virtual void __stdcall GetNumberOfFrame( int* f ) = 0; + virtual void __stdcall GetTotalTime( int64_t* t ) = 0; + + virtual void __stdcall GetVideoSize( long* width, long* height ) = 0; + virtual void __stdcall GetFrontBuffer( BYTE** buff ) = 0; + virtual void __stdcall SetVideoBuffer( BYTE* buff1, BYTE* buff2, long size ) = 0; + + virtual void __stdcall SetStopFrame( int frame ) = 0; + virtual void __stdcall GetStopFrame( int* frame ) = 0; + virtual void __stdcall SetDefaultStopFrame() = 0; + + virtual void __stdcall SetPlayRate( double rate ) = 0; + virtual void __stdcall GetPlayRate( double* rate ) = 0; + + virtual void __stdcall SetAudioBalance( long balance ) = 0; + virtual void __stdcall GetAudioBalance( long* balance ) = 0; + virtual void __stdcall SetAudioVolume( long volume ) = 0; + virtual void __stdcall GetAudioVolume( long* volume ) = 0; + + virtual void __stdcall GetNumberOfAudioStream( unsigned long* streamCount ) = 0; + virtual void __stdcall SelectAudioStream( unsigned long num ) = 0; + virtual void __stdcall GetEnableAudioStreamNum( long* num ) = 0; + virtual void __stdcall DisableAudioStream( void ) = 0; + + virtual void __stdcall GetNumberOfVideoStream( unsigned long* streamCount ) = 0; + virtual void __stdcall SelectVideoStream( unsigned long num ) = 0; + virtual void __stdcall GetEnableVideoStreamNum( long* num ) = 0; + + virtual void __stdcall SetMixingBitmap( HDC hdc, RECT* dest, float alpha ) = 0; + virtual void __stdcall ResetMixingBitmap() = 0; + + virtual void __stdcall SetMixingMovieAlpha( float a ) = 0; + virtual void __stdcall GetMixingMovieAlpha( float* a ) = 0; + virtual void __stdcall SetMixingMovieBGColor( unsigned long col ) = 0; + virtual void __stdcall GetMixingMovieBGColor( unsigned long *col ) = 0; + + virtual void __stdcall PresentVideoImage() = 0; + + virtual void __stdcall GetContrastRangeMin( float* v ) = 0; + virtual void __stdcall GetContrastRangeMax( float* v ) = 0; + virtual void __stdcall GetContrastDefaultValue( float* v ) = 0; + virtual void __stdcall GetContrastStepSize( float* v ) = 0; + virtual void __stdcall GetContrast( float* v ) = 0; + virtual void __stdcall SetContrast( float v ) = 0; + + virtual void __stdcall GetBrightnessRangeMin( float* v ) = 0; + virtual void __stdcall GetBrightnessRangeMax( float* v ) = 0; + virtual void __stdcall GetBrightnessDefaultValue( float* v ) = 0; + virtual void __stdcall GetBrightnessStepSize( float* v ) = 0; + virtual void __stdcall GetBrightness( float* v ) = 0; + virtual void __stdcall SetBrightness( float v ) = 0; + + virtual void __stdcall GetHueRangeMin( float* v ) = 0; + virtual void __stdcall GetHueRangeMax( float* v ) = 0; + virtual void __stdcall GetHueDefaultValue( float* v ) = 0; + virtual void __stdcall GetHueStepSize( float* v ) = 0; + virtual void __stdcall GetHue( float* v ) = 0; + virtual void __stdcall SetHue( float v ) = 0; + + virtual void __stdcall GetSaturationRangeMin( float* v ) = 0; + virtual void __stdcall GetSaturationRangeMax( float* v ) = 0; + virtual void __stdcall GetSaturationDefaultValue( float* v ) = 0; + virtual void __stdcall GetSaturationStepSize( float* v ) = 0; + virtual void __stdcall GetSaturation( float* v ) = 0; + virtual void __stdcall SetSaturation( float v ) = 0; + +// End: Add: T.Imoto +}; + +HMODULE krmovie; +void(*o_GetAPIVersion)(DWORD* version); +void(*o_GetMixingVideoOverlayObject)(HWND callbackwin, IStream* stream, const wchar_t * streamname, const wchar_t* type, uint64_t size, iTVPVideoOverlay** out); +void(*o_GetVideoLayerObject)(HWND callbackwin, IStream* stream, const wchar_t * streamname, const wchar_t* type, uint64_t size, iTVPVideoOverlay **out); +void(*o_GetVideoOverlayObject)(HWND callbackwin, IStream* stream, const wchar_t * streamname, const wchar_t* type, uint64_t size, iTVPVideoOverlay** out); +HRESULT(*o_V2Link)(iTVPFunctionExporter* exporter); +void(*o_V2Unlink)(); +const wchar_t *(*o_GetOptionDesc)(); + +ATOM __stdcall myRegisterClassExA(const WNDCLASSEXA* cls) +{ + puts("myRegisterClassExA"); + WNDCLASSEXA cls2 = *cls; + //cls2.style &= ~CS_PARENTDC; + ATOM reg = RegisterClassExA(&cls2); + return reg; +} + +//HWND theRealParent; + +BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam) +{ + DWORD wnd_pid; + DWORD my_pid = GetProcessId(GetCurrentProcess()); + GetWindowThreadProcessId(hwnd, &wnd_pid); + if (wnd_pid != my_pid) + return TRUE; + char buf[256]; + char buf2[256]; + GetClassName(hwnd, buf, 256); + GetWindowText(hwnd, buf2, 256); + RECT rect; + GetClientRect(hwnd, &rect); + const char * indent = " "+8-lParam; + printf("%sEEE=%p :: %s :: %s :: %p :: %d :: %ldx%ld\n", indent, hwnd, buf, buf2, GetAncestor(hwnd, GA_PARENT), IsWindowVisible(hwnd), rect.right, rect.bottom); + + WNDCLASSEXA wc; + GetClassInfoExA((HINSTANCE)GetWindowLongPtr(hwnd, GWLP_HINSTANCE), buf, &wc); + printf("%swcstyle=%.8x wndstyle=%.8lx %.8lx\n", indent, wc.style, (DWORD)GetWindowLong(hwnd, GWL_STYLE), (DWORD)GetWindowLong(hwnd, GWL_EXSTYLE)); + + HWND child = GetWindow(hwnd, GW_CHILD); + while (child) + { + EnumWindowsProc(child, lParam+2); + child = GetWindow(child, GW_HWNDNEXT); + } + return TRUE; +} +HWND __stdcall myCreateWindowExA(DWORD dwExStyle, LPCSTR lpClassName, LPCSTR lpWindowName, DWORD dwStyle, + int X, int Y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, LPVOID lpParam) +{ + + //HWND theRealParentsChild = GetWindow(hWndParent, GW_CHILD); + //SetWindowLong(hWndParent, GWL_STYLE, GetWindowLong(hWndParent, GWL_STYLE) & ~WS_CLIPCHILDREN); + //printf("HIDE=%p\n", theRealParentsChild); + //ShowWindow(theRealParentsChild, SW_HIDE); + + //theRealParent = hWndParent; + //HWND ret = hWndParent; + HWND ret = CreateWindowExA(dwExStyle, lpClassName, lpWindowName, dwStyle, X, Y, nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam); + //HWND ret = CreateWindowExA(dwExStyle, lpClassName, lpWindowName, dwStyle&~WS_CHILDWINDOW, X, Y, nWidth, nHeight, nullptr, hMenu, hInstance, lpParam); + printf("myCreateWindowExA %s %s PARENT=%p NEWCHILD=%p\n", lpClassName, lpWindowName, hWndParent, ret); + //EnumWindows(EnumWindowsProc, 0); + + return ret; +} +BOOL __stdcall myShowWindow(HWND hWnd, int nCmdShow) +{ + printf("myShowWindow %p %d\n", hWnd, nCmdShow); + return ShowWindow(hWnd, nCmdShow); + return TRUE; +} +BOOL __stdcall myDestroyWindow(HWND hWnd) +{ + printf("myDestroyWindow %p\n", hWnd); + //return TRUE; + BOOL g = DestroyWindow(hWnd); + //EnumWindows(EnumWindowsProc, 0); + return g; +} +BOOL __stdcall myMoveWindow(HWND hWnd, int X, int Y, int nWidth, int nHeight, BOOL bRepaint) +{ + printf("myMoveWindow %p\n", hWnd); + return MoveWindow(hWnd, X, Y, nWidth, nHeight, bRepaint); +} + +IDirect3DDevice9* g_d3ddev; +bool dump_texture(IDirect3DSurface9* renderTarget, const char * filename) +{ + HRESULT hr; + IDirect3DDevice9* dev; + //hr = dev->GetRenderTarget( 0, &renderTarget ); + hr = renderTarget->GetDevice(&dev); + //hr = lpPresInfo->lpSurf->GetSurfaceLevel( 0, &renderTarget ); + if( !renderTarget || FAILED(hr) ) + return false; + + D3DSURFACE_DESC rtDesc; + renderTarget->GetDesc( &rtDesc ); + + IDirect3DSurface9* resolvedSurface; + if( rtDesc.MultiSampleType != D3DMULTISAMPLE_NONE ) + { + hr = dev->CreateRenderTarget( rtDesc.Width, rtDesc.Height, rtDesc.Format, D3DMULTISAMPLE_NONE, 0, FALSE, &resolvedSurface, NULL ); + if( FAILED(hr) ) + return false; + hr = dev->StretchRect( renderTarget, NULL, resolvedSurface, NULL, D3DTEXF_NONE ); + if( FAILED(hr) ) + return false; + renderTarget = resolvedSurface; + } + + IDirect3DSurface9* offscreenSurface; + //hr = dev->CreateOffscreenPlainSurface( rtDesc.Width, rtDesc.Height, rtDesc.Format, D3DPOOL_SYSTEMMEM, &offscreenSurface, NULL ); + hr = dev->CreateOffscreenPlainSurface( rtDesc.Width, rtDesc.Height, D3DFMT_X8R8G8B8, D3DPOOL_SYSTEMMEM, &offscreenSurface, NULL ); + if( FAILED(hr) ) + return false; + + hr = dev->GetRenderTargetData( renderTarget, offscreenSurface ); + bool ok = SUCCEEDED(hr); + if( ok ) + { + // Here we have data in offscreenSurface. + D3DLOCKED_RECT lr; + RECT rect; + rect.left = 0; + rect.right = rtDesc.Width; + rect.top = 0; + rect.bottom = rtDesc.Height; + // Lock the surface to read pixels + hr = offscreenSurface->LockRect( &lr, &rect, D3DLOCK_READONLY ); + if( SUCCEEDED(hr) ) + { +FILE* f=fopen(filename, "wb"); +for (size_t i=0;iUnlockRect(); + } + else + { + ok = false; + } + } + return ok; +} + + + + +struct tTJSVariantString_S +{ + tjs_int RefCount; // reference count - 1 + tjs_char *LongString; + tjs_char ShortString[TJS_VS_SHORT_LEN +1]; + tjs_int Length; // string length + tjs_uint32 HeapFlag; + tjs_uint32 Hint; +}; +class tTJSVariantString : public tTJSVariantString_S {}; +struct tTJSString_S +{ + tTJSVariantString *Ptr; +}; +class tTJSString : public tTJSString_S {}; + +typedef tTJSString ttstr; + + + + +static void (*orig_TVPAddLog)(const ttstr & line); + +static void STDMETHODCALLTYPE TVPAddLog(const ttstr & line) +{ + //DebugBreak(); + printf("TVPAddLog: "); + wchar_t* x = line.Ptr->LongString ? line.Ptr->LongString : line.Ptr->ShortString; + while (*x) putchar(*x++); + puts(""); + //orig_TVPAddLog(line); +} +static void STDMETHODCALLTYPE TVPThrowExceptionMessage(const tjs_char * msg) +{ + printf("TVPThrowExceptionMessage: "); + while (*msg) + { + putchar(*msg); + msg++; + } + puts(""); + TerminateProcess(GetCurrentProcess(), 1); +} + +iTVPFunctionExporter* orig_exporter; +struct fancy_iTVPFunctionExporter : public iTVPFunctionExporter +{ + bool TJS_INTF_METHOD QueryFunctions(const tjs_char ** name, void ** function, tjs_uint count) override + { +puts("WIDEQUERY??"); + return orig_exporter->QueryFunctions(name, function, count); + } + bool TJS_INTF_METHOD QueryFunctionsByNarrowString(const char * * name, void* * function, tjs_uint count) override + { + bool ret = true; + for (tjs_uint i=0;iQueryFunctionsByNarrowString(&name[i], (void**)&orig_TVPAddLog, 1); + continue; + } + if (!strcmp(name[i], "void ::TVPThrowExceptionMessage(const tjs_char *)")) + { + function[i] = (void*)&TVPThrowExceptionMessage; + continue; + } + ret &= orig_exporter->QueryFunctionsByNarrowString(&name[i], &function[i], 1); + } + return ret; + } +}; +fancy_iTVPFunctionExporter my_exporter; + +iTVPVideoOverlay* orig_overlay; +class fancy_iTVPVideoOverlay : public iTVPVideoOverlay // this is not a COM object +{ +public: + virtual void __stdcall AddRef() { puts("fancy_iTVPVideoOverlay AddRef"); orig_overlay->AddRef(); } + virtual void __stdcall Release() { puts("fancy_iTVPVideoOverlay Release"); orig_overlay->Release(); puts("fancy_iTVPVideoOverlay Release done"); } + + virtual void __stdcall SetWindow(HWND window) + { + char buf[256]; + GetWindowTextA(window, buf, 256); + printf("fancy_iTVPVideoOverlay SetWindow %p %s\n", window, buf); + orig_overlay->SetWindow(window); + } + virtual void __stdcall SetMessageDrainWindow(HWND window) + { + char buf[256]; + GetWindowTextA(window, buf, 256); + printf("fancy_iTVPVideoOverlay SetMessageDrainWindow %p %s\n", window, buf); + orig_overlay->SetMessageDrainWindow(window); + } + virtual void __stdcall SetRect(RECT* rect) { puts("fancy_iTVPVideoOverlay SetRect"); orig_overlay->SetRect(rect); } + virtual void __stdcall SetVisible(bool b) { puts("fancy_iTVPVideoOverlay SetVisible"); orig_overlay->SetVisible(b); } + virtual void __stdcall Play() { puts("fancy_iTVPVideoOverlay Play"); orig_overlay->Play(); puts("Play done"); } + virtual void __stdcall Stop() { puts("fancy_iTVPVideoOverlay Stop"); orig_overlay->Stop(); puts("fancy_iTVPVideoOverlay Stop done"); } + virtual void __stdcall Pause() { puts("fancy_iTVPVideoOverlay Pause"); orig_overlay->Pause(); } + virtual void __stdcall SetPosition(uint64_t tick) { puts("fancy_iTVPVideoOverlay SetPosition"); orig_overlay->SetPosition(tick); } + virtual void __stdcall GetPosition(uint64_t* tick) { puts("fancy_iTVPVideoOverlay GetPosition"); orig_overlay->GetPosition(tick); } + virtual void __stdcall GetStatus(tTVPVideoStatus* status) { puts("fancy_iTVPVideoOverlay GetStatus"); orig_overlay->GetStatus(status); } + virtual void __stdcall GetEvent(long* evcode, long* param1, long* param2, bool* got) + { + //puts("fancy_iTVPVideoOverlay GetEvent"); + orig_overlay->GetEvent(evcode, param1, param2, got); + } + +// Start: Add: T.Imoto + virtual void __stdcall FreeEventParams(long evcode, long param1, long param2) + { + //puts("fancy_iTVPVideoOverlay FreeEventParams"); + orig_overlay->FreeEventParams(evcode, param1, param2); + } + + virtual void __stdcall Rewind() { puts("fancy_iTVPVideoOverlay Rewind"); orig_overlay->Rewind(); } + virtual void __stdcall SetFrame( int f ) { puts("fancy_iTVPVideoOverlay SetFrame"); orig_overlay->SetFrame(f); } + virtual void __stdcall GetFrame( int* f ) + { + //puts("fancy_iTVPVideoOverlay GetFrame"); + orig_overlay->GetFrame(f); +printf("frame=%d\n", *f); + } + virtual void __stdcall GetFPS( double* f ) { puts("fancy_iTVPVideoOverlay GetFPS"); orig_overlay->GetFPS(f); } + virtual void __stdcall GetNumberOfFrame( int* f ) { puts("fancy_iTVPVideoOverlay GetNumberOfFrame"); orig_overlay->GetNumberOfFrame(f); } + virtual void __stdcall GetTotalTime( int64_t* t ) { puts("fancy_iTVPVideoOverlay GetTotalTime"); orig_overlay->GetTotalTime(t); } + + virtual void __stdcall GetVideoSize( long* width, long* height ) { puts("fancy_iTVPVideoOverlay GetVideoSize"); orig_overlay->GetVideoSize(width, height); } + virtual void __stdcall GetFrontBuffer( BYTE** buff ) { puts("fancy_iTVPVideoOverlay GetFrontBuffer"); orig_overlay->GetFrontBuffer(buff); } + virtual void __stdcall SetVideoBuffer( BYTE* buff1, BYTE* buff2, long size ) { puts("fancy_iTVPVideoOverlay SetVideoBuffer"); orig_overlay->SetVideoBuffer(buff1, buff2, size); } + + virtual void __stdcall SetStopFrame( int frame ) { puts("fancy_iTVPVideoOverlay SetStopFrame"); orig_overlay->SetStopFrame(frame); } + virtual void __stdcall GetStopFrame( int* frame ) { puts("fancy_iTVPVideoOverlay GetStopFrame"); orig_overlay->GetStopFrame(frame); } + virtual void __stdcall SetDefaultStopFrame() { puts("fancy_iTVPVideoOverlay SetDefaultStopFrame"); orig_overlay->SetDefaultStopFrame(); } + + virtual void __stdcall SetPlayRate( double rate ) { puts("fancy_iTVPVideoOverlay SetPlayRate"); orig_overlay->SetPlayRate(rate); } + virtual void __stdcall GetPlayRate( double* rate ) { puts("fancy_iTVPVideoOverlay GetPlayRate"); orig_overlay->GetPlayRate(rate); } + + virtual void __stdcall SetAudioBalance( long balance ) { puts("fancy_iTVPVideoOverlay SetAudioBalance"); orig_overlay->SetAudioBalance(balance); } + virtual void __stdcall GetAudioBalance( long* balance ) { puts("fancy_iTVPVideoOverlay GetAudioBalance"); orig_overlay->GetAudioBalance(balance); } + virtual void __stdcall SetAudioVolume( long volume ) { puts("fancy_iTVPVideoOverlay SetAudioVolume"); orig_overlay->SetAudioVolume(volume); } + virtual void __stdcall GetAudioVolume( long* volume ) { puts("fancy_iTVPVideoOverlay GetAudioVolume"); orig_overlay->GetAudioVolume(volume); } + + virtual void __stdcall GetNumberOfAudioStream( unsigned long* streamCount ) { puts("fancy_iTVPVideoOverlay GetNumberOfAudioStream"); orig_overlay->GetNumberOfAudioStream(streamCount); } + virtual void __stdcall SelectAudioStream( unsigned long num ) { puts("fancy_iTVPVideoOverlay SelectAudioStream"); orig_overlay->SelectAudioStream(num); } + virtual void __stdcall GetEnableAudioStreamNum( long* num ) { puts("fancy_iTVPVideoOverlay GetEnableAudioStreamNum"); orig_overlay->GetEnableAudioStreamNum(num); } + virtual void __stdcall DisableAudioStream( void ) { puts("fancy_iTVPVideoOverlay DisableAudioStream"); orig_overlay->DisableAudioStream(); } + + virtual void __stdcall GetNumberOfVideoStream( unsigned long* streamCount ) { puts("fancy_iTVPVideoOverlay GetNumberOfVideoStream"); orig_overlay->GetNumberOfVideoStream(streamCount); } + virtual void __stdcall SelectVideoStream( unsigned long num ) { puts("fancy_iTVPVideoOverlay SelectVideoStream"); orig_overlay->SelectVideoStream(num); } + virtual void __stdcall GetEnableVideoStreamNum( long* num ) { puts("fancy_iTVPVideoOverlay GetEnableVideoStreamNum"); orig_overlay->GetEnableVideoStreamNum(num); } + + virtual void __stdcall SetMixingBitmap( HDC hdc, RECT* dest, float alpha ) { puts("fancy_iTVPVideoOverlay SetMixingBitmap"); orig_overlay->SetMixingBitmap(hdc, dest, alpha); } + virtual void __stdcall ResetMixingBitmap() { puts("fancy_iTVPVideoOverlay ResetMixingBitmap"); orig_overlay->ResetMixingBitmap(); } + + virtual void __stdcall SetMixingMovieAlpha( float a ) { puts("fancy_iTVPVideoOverlay SetMixingMovieAlpha"); orig_overlay->SetMixingMovieAlpha(a); } + virtual void __stdcall GetMixingMovieAlpha( float* a ) { puts("fancy_iTVPVideoOverlay GetMixingMovieAlpha"); orig_overlay->GetMixingMovieAlpha(a); } + virtual void __stdcall SetMixingMovieBGColor( unsigned long col ) { puts("fancy_iTVPVideoOverlay SetMixingMovieBGColor"); orig_overlay->SetMixingMovieBGColor(col); } + virtual void __stdcall GetMixingMovieBGColor( unsigned long *col ) { puts("fancy_iTVPVideoOverlay GetMixingMovieBGColor"); orig_overlay->GetMixingMovieBGColor(col); } + + virtual void __stdcall PresentVideoImage() + { + orig_overlay->PresentVideoImage(); + } + + virtual void __stdcall GetContrastRangeMin( float* v ) { puts("fancy_iTVPVideoOverlay GetContrastRangeMin"); orig_overlay->GetContrastRangeMin(v); } + virtual void __stdcall GetContrastRangeMax( float* v ) { puts("fancy_iTVPVideoOverlay GetContrastRangeMax"); orig_overlay->GetContrastRangeMax(v); } + virtual void __stdcall GetContrastDefaultValue( float* v ) { puts("fancy_iTVPVideoOverlay GetContrastDefaultValue"); orig_overlay->GetContrastDefaultValue(v); } + virtual void __stdcall GetContrastStepSize( float* v ) { puts("fancy_iTVPVideoOverlay GetContrastStepSize"); orig_overlay->GetContrastStepSize(v); } + virtual void __stdcall GetContrast( float* v ) { puts("fancy_iTVPVideoOverlay GetContrast"); orig_overlay->GetContrast(v); } + virtual void __stdcall SetContrast( float v ) { puts("fancy_iTVPVideoOverlay SetContrast"); orig_overlay->SetContrast(v); } + + virtual void __stdcall GetBrightnessRangeMin( float* v ) { puts("fancy_iTVPVideoOverlay GetBrightnessRangeMin"); orig_overlay->GetBrightnessRangeMin(v); } + virtual void __stdcall GetBrightnessRangeMax( float* v ) { puts("fancy_iTVPVideoOverlay GetBrightnessRangeMax"); orig_overlay->GetBrightnessRangeMax(v); } + virtual void __stdcall GetBrightnessDefaultValue( float* v ) { puts("fancy_iTVPVideoOverlay GetBrightnessDefaultValue"); orig_overlay->GetBrightnessDefaultValue(v); } + virtual void __stdcall GetBrightnessStepSize( float* v ) { puts("fancy_iTVPVideoOverlay GetBrightnessStepSize"); orig_overlay->GetBrightnessStepSize(v); } + virtual void __stdcall GetBrightness( float* v ) { puts("fancy_iTVPVideoOverlay GetBrightness"); orig_overlay->GetBrightness(v); } + virtual void __stdcall SetBrightness( float v ) { puts("fancy_iTVPVideoOverlay SetBrightness"); orig_overlay->SetBrightness(v); } + + virtual void __stdcall GetHueRangeMin( float* v ) { puts("fancy_iTVPVideoOverlay GetHueRangeMin"); orig_overlay->GetHueRangeMin(v); } + virtual void __stdcall GetHueRangeMax( float* v ) { puts("fancy_iTVPVideoOverlay GetHueRangeMax"); orig_overlay->GetHueRangeMax(v); } + virtual void __stdcall GetHueDefaultValue( float* v ) { puts("fancy_iTVPVideoOverlay GetHueDefaultValue"); orig_overlay->GetHueDefaultValue(v); } + virtual void __stdcall GetHueStepSize( float* v ) { puts("fancy_iTVPVideoOverlay GetHueStepSize"); orig_overlay->GetHueStepSize(v); } + virtual void __stdcall GetHue( float* v ) { puts("fancy_iTVPVideoOverlay GetHue"); orig_overlay->GetHue(v); } + virtual void __stdcall SetHue( float v ) { puts("fancy_iTVPVideoOverlay SetHue"); orig_overlay->SetHue(v); } + + virtual void __stdcall GetSaturationRangeMin( float* v ) { puts("fancy_iTVPVideoOverlay GetSaturationRangeMin"); orig_overlay->GetSaturationRangeMin(v); } + virtual void __stdcall GetSaturationRangeMax( float* v ) { puts("fancy_iTVPVideoOverlay GetSaturationRangeMax"); orig_overlay->GetSaturationRangeMax(v); } + virtual void __stdcall GetSaturationDefaultValue( float* v ) { puts("fancy_iTVPVideoOverlay GetSaturationDefaultValue"); orig_overlay->GetSaturationDefaultValue(v); } + virtual void __stdcall GetSaturationStepSize( float* v ) { puts("fancy_iTVPVideoOverlay GetSaturationStepSize"); orig_overlay->GetSaturationStepSize(v); } + virtual void __stdcall GetSaturation( float* v ) { puts("fancy_iTVPVideoOverlay GetSaturation"); orig_overlay->GetSaturation(v); } + virtual void __stdcall SetSaturation( float v ) { puts("fancy_iTVPVideoOverlay SetSaturation"); orig_overlay->SetSaturation(v); } + +// End: Add: T.Imoto +}; +fancy_iTVPVideoOverlay my_overlay; + +void my_init() +{ + if (!o_GetAPIVersion) + { + setvbuf(stdout, nullptr, _IONBF, 0); + + krmovie = LoadLibraryA("_rmovie.dll"); + if (!krmovie) + krmovie = LoadLibraryA("plugin/_rmovie.dll"); + o_GetAPIVersion = (decltype(o_GetAPIVersion))GetProcAddress(krmovie, "GetAPIVersion"); + o_GetMixingVideoOverlayObject = (decltype(o_GetMixingVideoOverlayObject))GetProcAddress(krmovie, "GetMixingVideoOverlayObject"); + o_GetVideoLayerObject = (decltype(o_GetVideoLayerObject))GetProcAddress(krmovie, "GetVideoLayerObject"); + o_GetVideoOverlayObject = (decltype(o_GetVideoOverlayObject))GetProcAddress(krmovie, "GetVideoOverlayObject"); + o_V2Link = (decltype(o_V2Link))GetProcAddress(krmovie, "V2Link"); + o_V2Unlink = (decltype(o_V2Unlink))GetProcAddress(krmovie, "V2Unlink"); + o_GetOptionDesc = (decltype(o_GetOptionDesc))GetProcAddress(krmovie, "GetOptionDesc"); + + if (false) + override_imports(krmovie, + [](const char * name) -> fptr { + //printf("import %s\n", name); + //if (!strcmp(name, "RegisterClassExA")) + //return (fptr)&myRegisterClassExA; + if (!strcmp(name, "CreateWindowExA")) + return (fptr)&myCreateWindowExA; + if (!strcmp(name, "DestroyWindow")) + return (fptr)&myDestroyWindow; + //if (!strcmp(name, "ShowWindow")) + //return (fptr)&myShowWindow; + if (!strcmp(name, "MoveWindow")) + return (fptr)&myMoveWindow; +#ifdef DOIT + //if (!strcmp(name, "CoCreateInstance")) + //return (fptr)&myCoCreateInstance; +#endif + return nullptr; + }); + } +} + +EXPORT_STDCALL(void, GetAPIVersion, (DWORD* version)) +{ + puts("krmovie GetAPIVersion"); + my_init(); + o_GetAPIVersion(version); +} +EXPORT_STDCALL(void, GetMixingVideoOverlayObject, (HWND callbackwin, IStream* stream, const wchar_t * streamname, + const wchar_t* type, uint64_t size, iTVPVideoOverlay** out)) +{ + printf("krmovie GetMixingVideoOverlayObject %ls %ls %p\n", streamname, type, callbackwin); + my_init(); + *out = &my_overlay; +#ifdef DOIT + o_GetMixingVideoOverlayObject(callbackwin, stream, streamname, type, size, &orig_overlay); +#else + o_GetMixingVideoOverlayObject(callbackwin, stream, streamname, type, size, out); +#endif +} +EXPORT_STDCALL(void, GetVideoLayerObject, (HWND callbackwin, IStream* stream, const wchar_t * streamname, + const wchar_t* type, uint64_t size, iTVPVideoOverlay **out)) +{ + printf("krmovie GetVideoLayerObject %ls %ls %p\n", streamname, type, callbackwin); + my_init(); + *out = &my_overlay; +#ifdef DOIT + o_GetVideoLayerObject(callbackwin, stream, streamname, type, size, &orig_overlay); +#else + o_GetVideoLayerObject(callbackwin, stream, streamname, type, size, out); +#endif +} +EXPORT_STDCALL(void, GetVideoOverlayObject, (HWND callbackwin, IStream* stream, const wchar_t * streamname, + const wchar_t* type, uint64_t size, iTVPVideoOverlay** out)) +{ + printf("krmovie GetVideoOverlayObject %ls %ls %p\n", streamname, type, callbackwin); + my_init(); + *out = &my_overlay; +#ifdef DOIT + o_GetVideoOverlayObject(callbackwin, stream, streamname, type, size, &orig_overlay); +#else + o_GetVideoOverlayObject(callbackwin, stream, streamname, type, size, out); +#endif +} +EXPORT_STDCALL(HRESULT, V2Link, (iTVPFunctionExporter* exporter)) +{ + puts("krmovie V2Link"); + my_init(); + orig_exporter = exporter; +//#ifdef DOIT + return o_V2Link(&my_exporter); +//#else + //return o_V2Link(exporter); +//#endif +} +EXPORT_STDCALL(void, V2Unlink, ()) +{ + puts("krmovie V2Unlink"); + my_init(); + o_V2Unlink(); +} +EXPORT_STDCALL(const wchar_t *, GetOptionDesc, ()) +{ + puts("krmovie GetOptionDesc"); + my_init(); + return o_GetOptionDesc(); +} diff --git a/gstkrkr.c b/gstkrkr.c new file mode 100644 index 0000000..a7ddaf9 --- /dev/null +++ b/gstkrkr.c @@ -0,0 +1,473 @@ +// SPDX-License-Identifier: LGPL-2.0-or-later + +#include +#include +#define PL_MPEG_IMPLEMENTATION +#include "pl_mpeg.h" + +/** + * SECTION:element-plugin + * + * FIXME:Describe plugin here. + * + * + * Example launch line + * |[ + * gst-launch-1.0 filesrc location=video.mpg ! mpegpsdemux name=demux ! mpegvideoparse ! avdec_mpeg2video ! queue ! autovideosink \ + * demux. ! mpegaudioparse ! avdec_mp2float ! audioconvert ! queue ! autoaudiosink + * gst-launch-1.0 filesrc location=video.mpg ! mpegpsdemux name=demux ! krkr_mpegvideo ! queue ! autovideosink \ + * demux. ! krkr_mpegaudio ! audioconvert ! queue ! autoaudiosink + * ]| + * + */ + +GST_DEBUG_CATEGORY_STATIC(gst_krkr_debug); +#define GST_CAT_DEFAULT gst_krkr_debug + +// in GStreamer, decoding a mpeg video requires three elements: mpegpsdemux ! mpegvideoparse ! avdec_mpeg2video +// in pl_mpeg, the latter two are merged to one object +// in DirectShow, the FORMER two are merged to one object +// (similar for audio, the elements are named mpegpsdemux ! mpegaudioparse ! avdec_mp2float) +// luckily, the parser elements just take byte sequences and return packets, consisting of the same bytes +// (but with packet boundaries at significant locations); stacking two mpegvideoparse elements is useless but harmless + +// rank rules: Must be higher than media-converter, and lower than every relevant official GStreamer filter. +// The relevant filters are +// - protonaudioconverter MARGINAL audio/x-wma +// - protonaudioconverterbin MARGINAL+1 audio/x-wma +// - protonvideoconverter MARGINAL video/x-ms-asf, video/x-msvideo, video/mpeg, video/quicktime +// - mpegpsdemux PRIMARY video/mpeg(systemstream=true) +// - mpegvideoparse PRIMARY+1 video/mpeg(systemstream=false) +// - avdec_mpeg2video PRIMARY video/mpeg(mpegversion=[1,2], systemstream=false, parsed=false) +// - mpegaudioparse PRIMARY+2 audio/mpeg(mpegversion=1, layer=2) +// - avdec_mp2float MARGINAL audio/mpeg(mpegversion=1, layer=2, parsed=true) +// - asfdemux SECONDARY video/x-ms-asf +// - avdec_wma* MARGINAL audio/x-wma +// - avdec_wmv* MARGINAL video/x-wmv + +// Most of those elements (or equivalents thereof) are present in Glorious Eggroll and easy to install. +// avdec_mp2float is gone, but mpg123audiodec works just as well. The only truly missing one is avdec_mpeg2video. +// However, audio/x-wma is troublesome - protonaudioconverterbin is above avdec_wma*. +// I can't change either of them, and I don't want to remove any files from Proton, so I'll have to do something ugly: +// A fake element whose only job is to create a real WMA decoder, with rank MARGINAL+2. + +// Sources are mostly gst-inspect-1.0; some source code is available at +// +// +// +// +// + +#define VIDEO_RANK GST_RANK_MARGINAL+1 // must be higher than protonvideoconverter (MARGINAL) and lower than avdec_mpeg2video (PRIMARY) +#define FAKEWMA_RANK GST_RANK_MARGINAL+2 // must be higher than protonaudioconverter (MARGINAL) and protonaudioconverterbin (MARGINAL+1) + +static void print_event(const char * pad_name, GstEvent* event) +{ + return; + fprintf(stderr, "gstkrkr: Received %s event on %s\n", GST_EVENT_TYPE_NAME(event), pad_name); + return; + gst_print("gstkrkr: Received %s event on %s: ", GST_EVENT_TYPE_NAME(event), pad_name); + + switch (GST_EVENT_TYPE(event)) + { + case GST_EVENT_CAPS: + { + GstCaps* caps; + gst_event_parse_caps(event, &caps); + gst_print("%" GST_PTR_FORMAT, caps); + break; + } + case GST_EVENT_SEGMENT: + { + const GstSegment * segment; + gst_event_parse_segment(event, &segment); + gst_print("%lu/%lu\n", (unsigned long)segment->position, (unsigned long)segment->duration); + break; + } + case GST_EVENT_TAG: + { + GstTagList* taglist; + gst_event_parse_tag(event, &taglist); + gst_print("%" GST_PTR_FORMAT, taglist); + break; + } + default: + gst_print("(unknown type)"); + } +} + +static void print_query(const char * pad_name, GstQuery* query) +{ + return; + fprintf(stderr, "gstkrkr: Received %s query on %s\n", GST_QUERY_TYPE_NAME(query), pad_name); +} + +static bool strbegin(const char * a, const char * b) +{ + return !strncmp(a, b, strlen(b)); +} + + + +#define GST_TYPE_PLMPEG_VIDEO (gst_krkr_video_get_type()) +G_DECLARE_FINAL_TYPE(GstKrkrPlMpegVideo, gst_krkr_video, GST, KRKRPLMPEG_VIDEO, GstElement) + +struct _GstKrkrPlMpegVideo +{ + GstElement element; + + GstPad* sinkpad; + GstPad* srcpad; + + int width; + int height; + + plm_buffer_t* buf; + plm_video_t* decode; +}; + +static GstStaticPadTemplate decodevideo_sink_factory = GST_STATIC_PAD_TEMPLATE( + "sink", + GST_PAD_SINK, + GST_PAD_ALWAYS, + GST_STATIC_CAPS("video/mpeg, mpegversion=(int)1, systemstream=(boolean)false") + ); + +static GstStaticPadTemplate decodevideo_src_factory = GST_STATIC_PAD_TEMPLATE( + "src", + GST_PAD_SRC, + GST_PAD_ALWAYS, + GST_STATIC_CAPS("video/x-raw, format=(string)YV12") + ); + +G_DEFINE_TYPE(GstKrkrPlMpegVideo, gst_krkr_video, GST_TYPE_ELEMENT); + +GST_ELEMENT_REGISTER_DECLARE(krkr_video); +GST_ELEMENT_REGISTER_DEFINE(krkr_video, "krkr_mpegvideo", VIDEO_RANK, GST_TYPE_PLMPEG_VIDEO); + +static void gst_krkr_video_finalize(GObject* object); + +static gboolean gst_krkr_video_sink_event(GstPad* pad, GstObject* parent, GstEvent* event); +static GstFlowReturn gst_krkr_video_sink_chain(GstPad* pad, GstObject* parent, GstBuffer* buf); +static gboolean gst_krkr_video_src_event(GstPad* pad, GstObject* parent, GstEvent* event); +static gboolean gst_krkr_video_src_query(GstPad* pad, GstObject* parent, GstQuery* query); + +static void gst_krkr_video_class_init(GstKrkrPlMpegVideoClass* klass) +{ + GObjectClass* gobject_class = G_OBJECT_CLASS(klass); + GstElementClass* gstelement_class = GST_ELEMENT_CLASS(klass); + + gobject_class->finalize = gst_krkr_video_finalize; + + gst_element_class_set_details_simple(gstelement_class, + "krkr_mpegvideo", + "Decoder/Video", + "MPEG-1 video decoder", + "Sir Walrus sir@walrus.se"); + + gst_element_class_add_pad_template(gstelement_class, gst_static_pad_template_get(&decodevideo_sink_factory)); + gst_element_class_add_pad_template(gstelement_class, gst_static_pad_template_get(&decodevideo_src_factory)); +} + +static void gst_krkr_video_init(GstKrkrPlMpegVideo* filter) +{ + filter->sinkpad = gst_pad_new_from_static_template(&decodevideo_sink_factory, "sink"); + gst_pad_set_event_function(filter->sinkpad, GST_DEBUG_FUNCPTR(gst_krkr_video_sink_event)); + gst_pad_set_chain_function(filter->sinkpad, GST_DEBUG_FUNCPTR(gst_krkr_video_sink_chain)); + GST_OBJECT_FLAG_SET(filter->sinkpad, GST_PAD_FLAG_NEED_PARENT); + gst_element_add_pad(GST_ELEMENT(filter), filter->sinkpad); + + filter->srcpad = gst_pad_new_from_static_template(&decodevideo_src_factory, "src"); + gst_pad_set_event_function(filter->srcpad, GST_DEBUG_FUNCPTR(gst_krkr_video_src_event)); + gst_pad_set_query_function(filter->srcpad, GST_DEBUG_FUNCPTR(gst_krkr_video_src_query)); + GST_OBJECT_FLAG_SET(filter->srcpad, GST_PAD_FLAG_NEED_PARENT); + gst_element_add_pad(GST_ELEMENT(filter), filter->srcpad); + + filter->buf = plm_buffer_create_with_capacity(PLM_BUFFER_DEFAULT_SIZE); + filter->decode = plm_video_create_with_buffer(filter->buf, false); + + //fprintf(stderr, "gstkrkr: Created a decoder\n"); +} + +static void gst_krkr_video_finalize(GObject* object) +{ + GstKrkrPlMpegVideo* filter = GST_KRKRPLMPEG_VIDEO(object); + + plm_buffer_destroy(filter->buf); + plm_video_destroy(filter->decode); + + G_OBJECT_CLASS(gst_krkr_video_parent_class)->finalize(object); +} + +static gboolean gst_krkr_video_sink_event(GstPad* pad, GstObject* parent, GstEvent* event) +{ + print_event("video sink", event); + + GstKrkrPlMpegVideo* filter = GST_KRKRPLMPEG_VIDEO(parent); + + switch (GST_EVENT_TYPE(event)) + { + case GST_EVENT_CAPS: + { + GstCaps* caps_in; + gst_event_parse_caps(event, &caps_in); + GstStructure* struc = gst_caps_get_structure(caps_in, 0); + + // expected input caps are + // video/mpeg, mpegversion=(int)1, systemstream=(boolean)false, parsed=(boolean)true, width=(int)640, height=(int)360, + // framerate=(fraction)30000/1001, pixel-aspect-ratio=(fraction)1/1, codec_data=(buffer)000001b328016814ffffe018 + // or + // video/mpeg, mpegversion=(int)1, systemstream=(boolean)false, parsed=(boolean)false + + // output from avdec_mpeg2video is + // video/x-raw, format=(string)YV12, width=(int)640, height=(int)360, interlace-mode=(string)progressive, + // pixel-aspect-ratio=(fraction)1/1, chroma-site=(string)jpeg, colorimetry=(string)2:0:0:0, framerate=(fraction)30000/1001 + // (I omit some of them) + int framerate_n; + int framerate_d; + if (gst_structure_get_int(struc, "width", &filter->width) && + gst_structure_get_int(struc, "height", &filter->height) && + gst_structure_get_fraction(struc, "framerate", &framerate_n, &framerate_d)) + { + GstCaps* caps_out = gst_caps_new_simple("video/x-raw", + "format", G_TYPE_STRING, "YV12", + "width", G_TYPE_INT, filter->width, + "height", G_TYPE_INT, filter->height, + "framerate", GST_TYPE_FRACTION, framerate_n, framerate_d, + NULL); + gst_pad_push_event(filter->srcpad, gst_event_new_caps(caps_out)); + } + else + { + filter->width = 0; + } + return TRUE; + } + case GST_EVENT_SEGMENT: + { + if (!filter->width) + return TRUE; + // fall through + } + default: + return gst_pad_event_default(pad, parent, event); + } +} + +static GstFlowReturn gst_krkr_video_sink_chain(GstPad* pad, GstObject* parent, GstBuffer* buf) +{ + GstKrkrPlMpegVideo* filter = GST_KRKRPLMPEG_VIDEO(parent); + for (size_t n=0;nbuf, meminf.data, meminf.size); + gst_memory_unref(mem); + } + + while (true) + { + plm_frame_t* frame = plm_video_decode(filter->decode); +//fprintf(stderr, "gstkrkr: decoded to %p\n", frame); + if (!frame) + break; + + if (!filter->width) + { + filter->width = frame->width; + filter->height = frame->height; + int framerate_n = plm_video_get_framerate(filter->decode) * 1000; + int framerate_d = 1000; + GstCaps* caps_out = gst_caps_new_simple("video/x-raw", + "format", G_TYPE_STRING, "YV12", + "width", G_TYPE_INT, filter->width, + "height", G_TYPE_INT, filter->height, + "framerate", GST_TYPE_FRACTION, framerate_n, framerate_d, + NULL); + gst_pad_push_event(filter->srcpad, gst_event_new_caps(caps_out)); + + GstEvent* segment = gst_pad_get_sticky_event(filter->sinkpad, GST_EVENT_SEGMENT, 0); + if (segment) + gst_pad_push_event(filter->srcpad, segment); + } + + size_t buflen = frame->width*frame->height*12/8; + uint8_t* ptr = g_malloc(buflen); + GstBuffer* buf = gst_buffer_new_wrapped(ptr, buflen); + for (int y=0;yheight;y++) + { + memcpy(ptr, frame->y.data + frame->y.width*y, frame->width); + ptr += frame->width; + } + for (int y=0;yheight/2;y++) + { + memcpy(ptr, frame->cr.data + frame->cr.width*y, frame->width/2); + ptr += frame->width/2; + } + for (int y=0;yheight/2;y++) + { + memcpy(ptr, frame->cb.data + frame->cb.width*y, frame->width/2); + ptr += frame->width/2; + } + + buf->pts = frame->time * 1000000000; + buf->dts = frame->time * 1000000000; + buf->duration = 1000000000 / plm_video_get_framerate(filter->decode); +//fprintf(stderr, "gstkrkr: send frame, %lu bytes\n", gst_buffer_get_size(buf)); + gst_pad_push(filter->srcpad, buf); + } + + return GST_FLOW_OK; +} + +static gboolean gst_krkr_video_src_event(GstPad* pad, GstObject* parent, GstEvent* event) +{ + print_event("video source", event); + + //GstKrkrPlMpegVideo* filter = GST_PLMPEG_VIDEO(parent); + + switch (GST_EVENT_TYPE(event)) { + case GST_EVENT_CAPS: + return TRUE; + default: + break; + } + return gst_pad_event_default(pad, parent, event); +} + +static gboolean gst_krkr_video_src_query(GstPad* pad, GstObject* parent, GstQuery* query) +{ + print_query("video src", query); + return gst_pad_query_default(pad, parent, query); +} + + + +#define GST_TYPE_KRKR_FAKEWMA_BASE (gst_krkr_fakewma_get_type()) +G_DECLARE_DERIVABLE_TYPE(GstKrkrFakeWma, gst_krkr_fakewma, GST, KRKR_FAKEWMA_BASE, GstBin) + +struct _GstKrkrFakeWmaClass +{ + GstBinClass parent_class; + GstStaticPadTemplate sink_template; +}; + +static GstStaticPadTemplate fakewma_src_factory = GST_STATIC_PAD_TEMPLATE("src", GST_PAD_SRC, GST_PAD_ALWAYS, GST_STATIC_CAPS("audio/x-raw")); + +G_DEFINE_TYPE(GstKrkrFakeWma, gst_krkr_fakewma, GST_TYPE_BIN); + +static void gst_krkr_fakewma_class_init(GstKrkrFakeWmaClass* klass) {} + +static void gst_krkr_fakewma_class_init_child(GstKrkrFakeWmaClass* klass) +{ + const char * name = g_type_get_qdata(G_OBJECT_CLASS_TYPE(klass), g_quark_from_static_string("krkrwine_fakewma_namecaps")); + const char * caps = name + strlen(name) + 1; + + klass->sink_template = (GstStaticPadTemplate)GST_STATIC_PAD_TEMPLATE("sink", GST_PAD_SINK, GST_PAD_ALWAYS, GST_STATIC_CAPS(caps)); + + GstElementClass* gstelement_class = GST_ELEMENT_CLASS(klass); + gst_element_class_set_details_simple(gstelement_class, + name, + "Decoder/Audio", + "Fake WMA decoder, loads another one while avoiding protonaudioconverter", + "Sir Walrus sir@walrus.se"); + + gst_element_class_add_pad_template(gstelement_class, gst_static_pad_template_get(&klass->sink_template)); + gst_element_class_add_pad_template(gstelement_class, gst_static_pad_template_get(&fakewma_src_factory)); +} + +static GstElement* get_decoder_for(GstCaps* caps) +{ + GList* transforms = gst_element_factory_list_get_elements(GST_ELEMENT_FACTORY_TYPE_DECODER, GST_RANK_MARGINAL); + GList* transforms2 = gst_element_factory_list_filter(transforms, caps, GST_PAD_SINK, FALSE); + gst_plugin_feature_list_free(transforms); + GstElement* ret = NULL; + + for (int pass=0;pass<2;pass++) + { + for (GList* iter = transforms2; iter && !ret; iter = iter->next) + { + GstPluginFeature* feat = GST_PLUGIN_FEATURE(iter->data); + GstElementFactory* fac = GST_ELEMENT_FACTORY(iter->data); + const char * name = gst_plugin_feature_get_name(feat); + + if (strbegin(name, "krkr_fake")) // discard ourselves, that's just recursion + continue; + if (pass == 0 && strbegin(name, "proton")) // accept it if nothing else exists, better than crashing... + continue; + + ret = gst_element_factory_create(fac, gst_plugin_feature_get_name(feat)); + } + } + gst_plugin_feature_list_free(transforms2); + return ret; +} + +static void gst_krkr_fakewma_init(GstKrkrFakeWma* filter) {} + +static void gst_krkr_fakewma_init_child(GstKrkrFakeWma* filter) +{ + //fprintf(stderr, "gstkrkr: Created a fakewma\n"); + + GstKrkrFakeWmaClass* klass = (GstKrkrFakeWmaClass*)G_OBJECT_GET_CLASS(filter); + + GstCaps* caps = gst_static_pad_template_get_caps(&klass->sink_template); + GstElement* real_decoder = get_decoder_for(caps); + gst_caps_unref(caps); + gst_bin_add(GST_BIN(filter), real_decoder); + + GstPad* sinkpad = gst_ghost_pad_new("sink", gst_element_get_static_pad(real_decoder, "sink")); + gst_element_add_pad(GST_ELEMENT(filter), sinkpad); + + GstPad* srcpad = gst_ghost_pad_new("src", gst_element_get_static_pad(real_decoder, "src")); + gst_element_add_pad(GST_ELEMENT(filter), srcpad); +} + +static gboolean gst_krkr_fakewma_type_create(GstPlugin* plugin, const char * namecaps) +{ + GType this_type = g_type_register_static_simple( + GST_TYPE_KRKR_FAKEWMA_BASE, + g_intern_static_string(namecaps), + sizeof(GstKrkrFakeWmaClass), + (GClassInitFunc)gst_krkr_fakewma_class_init_child, + sizeof(GstKrkrFakeWma), + (GInstanceInitFunc)gst_krkr_fakewma_init_child, + G_TYPE_FLAG_NONE); + g_type_set_qdata(this_type, g_quark_from_static_string("krkrwine_fakewma_namecaps"), (void*)namecaps); + + return gst_element_register(plugin, namecaps, FAKEWMA_RANK, this_type); +} + + + +static gboolean plugin_init(GstPlugin* plugin) +{ + GST_DEBUG_CATEGORY_INIT(gst_krkr_debug, "plugin", 0, "krkrwine plugin"); + return GST_ELEMENT_REGISTER(krkr_video, plugin) && + gst_krkr_fakewma_type_create(plugin, "krkr_fakewmav1\0audio/x-wma, wmaversion=(int)1") && + gst_krkr_fakewma_type_create(plugin, "krkr_fakewmav2\0audio/x-wma, wmaversion=(int)2") && + gst_krkr_fakewma_type_create(plugin, "krkr_fakewmav3\0audio/x-wma, wmaversion=(int)3") && + gst_krkr_fakewma_type_create(plugin, "krkr_fakewmalossless\0audio/x-wma, wmaversion=(int)4"); +} + +#ifndef PLUGINARCH +#error Need to define PLUGINARCH, did you typo the makefile? +#endif +#ifdef i386 +#error Must compile with -std=c##, not gnu## +#endif + +#define PACKAGE "krkrwine" +GST_PLUGIN_DEFINE( + // overriding the version like this makes me a Bad Person(tm), but Proton 8.0 is on 1.18.5, so I need to stay behind + 1, // GST_VERSION_MAJOR, + 18, // GST_VERSION_MINOR, + G_PASTE(krkr_, PLUGINARCH), + "krkrwine module for GStreamer", + plugin_init, + "1.0", + "LGPL", + "gstkrkr", + "https://walrus.se/" +) diff --git a/install.py b/install.py new file mode 100755 index 0000000..27f2b75 --- /dev/null +++ b/install.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: LGPL-2.0-or-later + +import argparse, subprocess, os, shutil + +CLSID_FilterGraph = "{E436EBB3-524F-11CE-9F53-0020AF0BA770}" +CLSID_FilterGraphNoThread = "{E436EBB8-524F-11CE-9F53-0020AF0BA770}" + +def key_clsid(clsid, suffix="\\InprocServer32"): + return "Software\\Classes\\CLSID\\"+clsid+suffix +def key_clsid_wow64(clsid, suffix="\\InprocServer32"): + return "Software\\Classes\\Wow6432Node\\CLSID\\"+clsid+suffix + +def escape_regstr(key): + return key.replace("\\","\\\\") +def unescape_regstr(key): + return key.replace("\\\\","\\") +def find_regkey(registry, key): + idx1 = registry.index("["+escape_regstr(key)+"]") + idx2 = registry.index("\n@=", idx1)+3 + idx3 = registry.index("\n", idx2) + return idx2, idx3 +def get_regkey(registry, key): + idx2, idx3 = find_regkey(registry, key) + val = registry[idx2:idx3] + if val.startswith('"') and val.endswith('"'): + return unescape_regstr(val[1:-1]) + else: + 1/0 +def set_regkey(registry, key, value): + idx2, idx3 = find_regkey(registry, key) + return registry[:idx2] + '"'+escape_regstr(value)+'"' + registry[idx3:] + +def silent_unlink(path): + try: + os.unlink(path) + except FileNotFoundError: + pass + +def in_this_dir(name): + path = os.path.realpath(os.path.dirname(__file__)+"/"+name) + assert os.path.isfile(path) + return path + +def set_regkeys(wineprefix, dllpaths): + registry = open(wineprefix+"/system.reg","rt").read() + registry = set_regkey(registry, key_clsid(CLSID_FilterGraph), dllpaths["x86_64"]) + registry = set_regkey(registry, key_clsid(CLSID_FilterGraphNoThread), dllpaths["x86_64"]) + registry = set_regkey(registry, key_clsid_wow64(CLSID_FilterGraph), dllpaths["i386"]) + registry = set_regkey(registry, key_clsid_wow64(CLSID_FilterGraphNoThread), dllpaths["i386"]) + open(wineprefix+"/system.reg","wt").write(registry) + + +def gstreamer_install(install, devel): + gst_dir = os.getenv("HOME")+"/.local/share/gstreamer-1.0/plugins" + os.makedirs(gst_dir, exist_ok=True) + silent_unlink(gst_dir+"/gstkrkr-i386.so") + silent_unlink(gst_dir+"/gstkrkr-x86_64.so") + if uninstall: + pass + elif devel: + os.symlink(in_this_dir("gstkrkr-i386.so"), gst_dir + "/gstkrkr-i386.so") + os.symlink(in_this_dir("gstkrkr-x86_64.so"), gst_dir + "/gstkrkr-x86_64.so") + else: + shutil.copy(in_this_dir("gstkrkr-i386.so"), gst_dir + "/gstkrkr-i386.so") + shutil.copy(in_this_dir("gstkrkr-x86_64.so"), gst_dir + "/gstkrkr-x86_64.so") + + +def wine_install(wineprefix, unixpaths, unsafe, mode): + if not unsafe: + procs = subprocess.run(["ps", "-ef"], stdout=subprocess.PIPE).stdout + if b'\\windows' in procs or b'wineserver' in procs or b'winedevice' in procs: + print("Wine is currently running, close all Wine programs then run wineserver -k (if you did that already, kill them manually)") + exit(1) + + if mode == "z": + set_regkeys(wineprefix, { "x86_64": "Z:"+unixpaths["x86_64"].replace("/","\\"), "i386": "Z:"+unixpaths["i386"].replace("/","\\") }) + elif mode == "symlink": + set_regkeys(wineprefix, { "x86_64": "C:\\windows\\system32\\krkrwine.dll", "i386": "C:\\windows\\system32\\krkrwine.dll" }) + silent_unlink(wineprefix + "/drive_c/windows/system32/krkrwine.dll") + silent_unlink(wineprefix + "/drive_c/windows/syswow64/krkrwine.dll") + os.symlink(unixpaths["x86_64"], wineprefix + "/drive_c/windows/system32/krkrwine.dll") + os.symlink(unixpaths["i386"], wineprefix + "/drive_c/windows/syswow64/krkrwine.dll") + elif mode == "copy": + set_regkeys(wineprefix, { "x86_64": "C:\\windows\\system32\\krkrwine.dll", "i386": "C:\\windows\\system32\\krkrwine.dll" }) + shutil.copy(unixpaths["x86_64"], wineprefix + "/drive_c/windows/system32/krkrwine.dll") + shutil.copy(unixpaths["i386"], wineprefix + "/drive_c/windows/syswow64/krkrwine.dll") + else: + 1/0 + +def wine_uninstall(wineprefix): + set_regkeys(wineprefix, { "x86_64": "C:\\windows\\system32\\quartz.dll", "i386": "C:\\windows\\system32\\quartz.dll" }) + silent_unlink(wineprefix + "/drive_c/windows/system32/krkrwine.dll") + silent_unlink(wineprefix + "/drive_c/windows/syswow64/krkrwine.dll") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('path', type=str, nargs='?', action="store", help="Path to Proton installation") + parser.add_argument('--wine', action="store_true", help="Install to non-Protom Wine at ~/.wine or WINEPREFIX") + parser.add_argument('--unsafe', action="store_true", help="Don't check if Wine is running before installing") + parser.add_argument('--with-gstreamer', action="store_true", help="Install GStreamer plugins as well (not recommended, unless for development purposes; Wine only, not Proton)") + parser.add_argument('--uninstall', action="store_true", help="Uninstall instead (Wine only, not Proton - sorry about that)") + parser.add_argument('--devel', action="store_true", help="Point Wine to the DLLs' current location, instead of copying them into Wine's system32; allows easier updates of them, but you can't remove the krkrwine download directory (Wine only, not Proton)") + + args = parser.parse_args() + + if args.path is None and not args.wine: + print(""" +Usage: +./install.py ~/steam/steamapps/common/Proton\ 8.0/ # install krkrwine into every current and future game installed into that Proton version +./install.py --wine # install into a non-Proton Wine + """.strip()) + exit(0) + + if args.path is not None and args.wine: + print("Can't specify both Proton path and installing to Wine; to specify the Wine path, set WINEPREFIX") + exit(1) + + unixpaths = { "x86_64": in_this_dir("krkrwine-x86_64.dll"), "i386": in_this_dir("krkrwine-i386.dll") } + + if args.wine: + wineprefix = os.getenv("WINEPREFIX") + if not wineprefix: + wineprefix = os.getenv("HOME")+"/.wine" + + if not os.path.isfile(wineprefix + "/system.reg"): + print("That doesn't look like a Wine prefix") + exit(1) + + if not args.uninstall: + dllpaths = {} + wine_install(wineprefix, unixpaths=unixpaths, unsafe=args.unsafe, mode=["copy", "z"][args.devel]) + if args.with_gstreamer: + gstreamer_install(True, args.devel) + else: + wine_uninstall(wineprefix) + gstreamer_install(False, False) + else: + if os.path.isfile(args.path + "/dist/share/default_pfx/system.reg"): + # normal Proton + proton_files = args.path + "/dist" + elif os.path.isfile(args.path + "/files/share/default_pfx/system.reg"): + # Glorious Eggroll (I don't know if the community Proton uses dist or files) + proton_files = args.path + "/files" + elif os.path.isfile(args.path + "/pfx/system.reg"): + # it's a Proton prefix + print("Don't do that, install to the Proton installation instead") + exit(1) + else: + print("That doesn't look like Proton") + exit(1) + + if args.uninstall: + print("sorry, unimplemented - krkrwine installs itself to every Proton prefix where it's used, it's hard to identify which prefixes to uninstall from") + exit(1) + + print("Installing krkrwine...") + + # it's a Proton installation + wine_install(proton_files + "/share/default_pfx", unixpaths=unixpaths, unsafe=True, mode="copy") + + import_me = "import krkrwine\nkrkrwine.install_within_proton()\n" + shutil.copy(__file__, args.path + "/krkrwine.py") + try: + prev_user_settings = open(args.path + "/user_settings.py", "rt").read() + except FileNotFoundError: + prev_user_settings = "user_settings = {}\n" + if import_me not in prev_user_settings: + open(args.path + "/user_settings.py", "wt").write(import_me + prev_user_settings) + + shutil.copy(in_this_dir("gstkrkr-x86_64.so"), proton_files + "/lib64/gstreamer-1.0/gstkrkr-x86_64.so") + shutil.copy(in_this_dir("gstkrkr-i386.so"), proton_files + "/lib/gstreamer-1.0/gstkrkr-i386.so") + + eggroll_files = [ + "lib64/gstreamer-1.0/libgstmpegpsdemux.so", "lib/gstreamer-1.0/libgstmpegpsdemux.so", + "lib64/gstreamer-1.0/libgstasf.so", "lib/gstreamer-1.0/libgstasf.so", + "lib64/libgstcodecparsers-1.0.so.0", "lib/libgstcodecparsers-1.0.so.0", + "lib64/libavcodec.so.58", "lib/libavcodec.so.58", + "lib64/libavutil.so.56", "lib/libavutil.so.56", + "lib64/libavfilter.so.7", "lib/libavfilter.so.7", + "lib64/libavformat.so.58", "lib/libavformat.so.58", + "lib64/libavdevice.so.58", "lib/libavdevice.so.58", + "lib64/libswresample.so.3", "lib/libswresample.so.3", + "lib64/libswscale.so.5", "lib/libswscale.so.5", + ] + if all([os.path.isfile(proton_files+"/"+f) for f in eggroll_files]): + print("Skipping Glorious Eggroll components, they're already in place") + pass # if the Glorious Eggroll components are already in place (for example because this is a GE, or it's installed already), just leave them + elif any([os.path.isfile(proton_files+"/"+f) for f in eggroll_files]): + print("Partial Glorious Eggroll components installation? Either Proton updated in a way krkrwine doesn't recognize, or your installation is damaged") + exit(1) + else: + print("Installing Glorious Eggroll components...") + for dst_name in eggroll_files: + src_name = "Glorious-Eggroll/"+dst_name.replace("gstreamer-1.0/","").replace("lib/","i386-").replace("lib64/","x86_64-") + shutil.copy(in_this_dir(src_name), proton_files+"/"+dst_name) + + print("The operation completed successfully") + +def install_within_proton(): + wineprefix = os.environ["STEAM_COMPAT_DATA_PATH"] + "/pfx" + unixpaths = {} + unixpaths["x86_64"] = in_this_dir("dist/share/default_pfx/drive_c/windows/system32/krkrwine.dll") + unixpaths["i386"] = in_this_dir("dist/share/default_pfx/drive_c/windows/syswow64/krkrwine.dll") + if os.path.isdir(wineprefix): + # if not, the registry and DLLs will be copied from the upstream Proton installation + wine_install(wineprefix, unixpaths=unixpaths, unsafe=True, mode="symlink") + os.environ["PROTON_NO_STEAM_FFMPEG"] = "1" diff --git a/krkrwine.cpp b/krkrwine.cpp new file mode 100644 index 0000000..28b38a3 --- /dev/null +++ b/krkrwine.cpp @@ -0,0 +1,2326 @@ +// SPDX-License-Identifier: LGPL-2.0-or-later + +#define DOIT + +#ifdef __MINGW32__ +# define _FILE_OFFSET_BITS 64 +// mingw *really* wants to define its own printf/scanf, which adds ~20KB random stuff to the binary +// (on some 32bit mingw versions, it also adds a dependency on libgcc_s_sjlj-1.dll) +// extra kilobytes and dlls is the opposite of what I want, and my want is stronger, so here's some shenanigans +// comments say libstdc++ demands a POSIX printf, but I don't use libstdc++'s text functions, so I don't care +# define __USE_MINGW_ANSI_STDIO 0 // trigger a warning if it's enabled already - probably wrong include order +# include // include some random c++ header; they all include , +# undef __USE_MINGW_ANSI_STDIO // which ignores my #define above and sets this flag; re-clear it before including +# define __USE_MINGW_ANSI_STDIO 0 // (subsequent includes of c++config.h are harmless, there's an include guard) +#endif + +// WARNING to anyone trying to use these objects to implement them for real in Wine: +// Wine's CLSID_CMpegAudioCodec demands packet boundaries to be in place. Therefore, this demuxer does that. +// It's as if there's a builtin mpegaudioparse element. +// However, the demuxer does NOT do the same for video data! +// The demuxer does parse the video info somewhat, but only to discover the resolution. There is no builtin mpegvideoparse; +// that functionality is instead part of the video decoder. +// This works fine for me, since this demuxer's video pin only needs to connect to the matching video decoder, +// but it may look confusing to anyone investigating either object on its own. +// Additionally, WMCreateSyncReader, CLSID_CWMVDecMediaObject and CLSID_CWMADecMediaObject send very different data +// between each other than the corresponding objects on Windows; you cannot use any of them to implement the others. +// Finally, these objects do not work on Windows; after attaching the output pin, +// they segfault somewhere deep inside quartz.dll. I have not been able to determine why. + +#define INITGUID +#define STRSAFE_NO_DEPRECATE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#define PL_MPEG_IMPLEMENTATION +#include "pl_mpeg.h" + +static const GUID GUID_NULL = {}; // not defined in my headers, how lovely + +// some of these guids aren't defined in my headers, and some are only defined in some ways +// for example, __uuidof(IUnknown) is defined, but IID_IUnknown is not; IID_IAMStreamSelect is defined, but __uuidof(IAMStreamSelect) is not +DEFINE_GUID(IID_IUnknown, 0x00000000, 0x0000, 0x0000, 0xc0,0x00, 0x00,0x00,0x00,0x00,0x00,0x46); +DEFINE_GUID(IID_IAMOpenProgress,0x8E1C39A1, 0xDE53, 0x11cf, 0xAA, 0x63, 0x00, 0x80, 0xC7, 0x44, 0x52, 0x8D); +DEFINE_GUID(IID_IAMDeviceRemoval,0xf90a6130,0xb658,0x11d2,0xae,0x49,0x00,0x00,0xf8,0x75,0x4b,0x99); +__CRT_UUID_DECL(IAMStreamSelect, 0xc1960960, 0x17f5, 0x11d1, 0xab,0xe1, 0x00,0xa0,0xc9,0x05,0xf3,0x75) +// used only to detect DXVK +DEFINE_GUID(IID_ID3D9VkInteropDevice, 0x2eaa4b89, 0x0107, 0x4bdb, 0x87,0xf7, 0x0f,0x54,0x1c,0x49,0x3c,0xe0); + +static char* guid_to_str(const GUID& guid) +{ + static char buf[8][64]; + static int n = 0; + char* ret = buf[n++%8]; + sprintf(ret, "{%08lX-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}", guid.Data1, guid.Data2, guid.Data3, guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3], guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]); + return ret; +} + +template T min(T a, T b) { return a < b ? a : b; } +template T max(T a, T b) { return a > b ? a : b; } + + +template class CComPtr { + void assign(T* ptr) + { + p = ptr; + } + void release() + { + if (p) + p->Release(); + p = nullptr; + } +public: + T* p; + + CComPtr() { p = nullptr; } + ~CComPtr() { release(); } + CComPtr(const CComPtr&) = delete; + CComPtr(CComPtr&&) = delete; + void operator=(const CComPtr&) = delete; + void operator=(CComPtr&&) = delete; + + CComPtr& operator=(T* ptr) + { + release(); + assign(ptr); + return *this; + } + T** operator&() + { + release(); + return &p; + } + T* operator->() { return p; } + operator T*() { return p; } + + HRESULT CoCreateInstance(REFCLSID rclsid, LPUNKNOWN pUnkOuter, DWORD dwClsContext) + { + release(); + return ::CoCreateInstance(rclsid, pUnkOuter, dwClsContext, IID_PPV_ARGS(&p)); + } + template + HRESULT QueryInterface(T2** other) + { + return p->QueryInterface(IID_PPV_ARGS(other)); + } + HRESULT CopyTo(T** ppT) + { + p->AddRef(); + *ppT = p; + return S_OK; + } +}; + +template T first_helper(); +template +class com_base_embedded : public Tis... { +private: + template bool QueryInterfaceSingle(REFIID riid, void** ppvObject) + { + if (riid == __uuidof(Ti)) + { + Ti* ret = this; + ret->AddRef(); + *ppvObject = (void*)ret; + return true; + } + else return false; + } +public: + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override + { +//printf("plm QI %s\n", guid_to_str(riid)); + *ppvObject = nullptr; + if (riid == __uuidof(IUnknown)) + { + IUnknown* ret = (decltype(first_helper())*)this; + ret->AddRef(); + *ppvObject = (void*)ret; + return S_OK; + } + return (QueryInterfaceSingle(riid, ppvObject) || ...) ? S_OK : E_NOINTERFACE; + } +}; + +//static uint32_t n_combase_objects = 0; +template +class com_base : public com_base_embedded { + uint32_t refcount = 1; +public: + ULONG STDMETHODCALLTYPE AddRef() override + { +//if (!strcmp(typeid(this).name(), "P8com_baseIJ11IBaseFilter27IVMRSurfaceAllocatorNotify9EE")) +//{ + //printf("addref, now %u\n", refcount+1); +//} + return ++refcount; + } + ULONG STDMETHODCALLTYPE Release() override + { +//if (!strcmp(typeid(this).name(), "P8com_baseIJ11IBaseFilter27IVMRSurfaceAllocatorNotify9EE")) +//{ + //printf("release, now %u\n", refcount-1); +//} + uint32_t new_refcount = --refcount; + if (!new_refcount) + delete this; + return new_refcount; + } + com_base() + { + // there's some weird race condition here... haven't bothered tracking it down + // Wagamama High Spec Trial Edition segfaults on launch if I remove it + Sleep(1); + //DWORD n = InterlockedIncrement(&n_combase_objects); + //printf("created a %s at %p, now %lu objects\n", typeid(this).name(), this, n); + } + virtual ~com_base() + { + //DWORD n = InterlockedDecrement(&n_combase_objects); + //printf("deleted a %s at %p, now %lu objects\n", typeid(this).name(), this, n); + } +}; + +template +Tinner com_enum_helper(HRESULT STDMETHODCALLTYPE (Touter::*)(ULONG, Tinner**, ULONG*)); + +// don't inline this lambda into the template's default argument, gcc bug 105667 +static const auto addref_ptr = [](T* obj) { obj->AddRef(); return obj; }; +template +class com_enum : public com_base { + using Tret = decltype(com_enum_helper(&Tinterface::Next)); + + Tret** items; + size_t pos = 0; + size_t len; + + com_enum(Tret** items, size_t pos, size_t len) : items(items), pos(pos), len(len) {} +public: + com_enum(Tret** items, size_t len) : items(items), len(len) {} + + HRESULT STDMETHODCALLTYPE Clone(Tinterface** ppEnum) override + { + *ppEnum = new com_enum(items, pos, len); + return S_OK; + } + HRESULT STDMETHODCALLTYPE Next(ULONG celt, Tret** rgelt, ULONG* pceltFetched) override + { + size_t remaining = len - pos; + size_t ret = min((size_t)celt, remaining); + for (size_t n=0;n= celt) + return S_OK; + else + return S_FALSE; + } + HRESULT STDMETHODCALLTYPE Reset() override + { + pos = 0; + return S_OK; + } + HRESULT STDMETHODCALLTYPE Skip(ULONG celt) override + { + size_t remaining = len - pos; + if (remaining >= celt) + { + pos += celt; + return S_OK; + } + else + { + pos = len; + return S_FALSE; + } + } +}; + + +// inspired by the Linux kernel macro, but using a member pointer looks cleaner; non-expressions (like member names) in macros look wrong +template Tc* container_of(Ti* ptr, Ti Tc:: * memb) +{ + // https://wg21.link/P0908 proposes a better implementation, but it was forgotten and not accepted + Tc* fake_object = (Tc*)0x12345678; // doing math on a fake pointer is UB, but good luck proving it's bogus + size_t offset = (uintptr_t)&(fake_object->*memb) - (uintptr_t)fake_object; + return (Tc*)((uint8_t*)ptr - offset); +} +template const Tc* container_of(const Ti* ptr, Ti Tc:: * memb) +{ + return container_of((Ti*)ptr, memb); +} +template auto container_of(Ti* ptr) { return container_of(ptr, memb); } + + +template +HRESULT qi_release(T* obj, REFIID riid, void** ppvObj) +{ + HRESULT hr = obj->QueryInterface(riid, ppvObj); + obj->Release(); + return hr; +} + + +static HRESULT CopyMediaType(AM_MEDIA_TYPE * pmtTarget, const AM_MEDIA_TYPE * pmtSource) +{ + *pmtTarget = *pmtSource; + if (pmtSource->pbFormat != nullptr) + { + pmtTarget->pbFormat = (uint8_t*)CoTaskMemAlloc(pmtSource->cbFormat); + memcpy(pmtTarget->pbFormat, pmtSource->pbFormat, pmtSource->cbFormat); + } + return S_OK; +} + +static AM_MEDIA_TYPE* CreateMediaType(const AM_MEDIA_TYPE * pSrc) +{ + AM_MEDIA_TYPE* ret = (AM_MEDIA_TYPE*)CoTaskMemAlloc(sizeof(AM_MEDIA_TYPE)); + CopyMediaType(ret, pSrc); + return ret; +} + +static void convert_rgb24_to_rgb32(uint8_t * dst, const uint8_t * src, size_t n_pixels) +{ + for (size_t n=0;n +class base_filter : public com_base { +public: + void debug(const char * fmt, ...) + { + return; + char buf[1024]; + + va_list args; + va_start(args, fmt); + vsnprintf(buf, 1024, fmt, args); + va_end(args); + +#ifdef __cpp_rtti + const char * my_typename = typeid(Touter).name(); +#else + const char * my_typename = ""; +#endif + fprintf(stdout, "plm %lu %s %s\n", GetCurrentThreadId(), my_typename, buf); + fflush(stdout); + } + + Touter* parent() + { + return (Touter*)this; + } + + // several pointers in here aren't CComPtr, due to reference cycles + IFilterGraph* graph = nullptr; + + FILTER_STATE state = State_Stopped; + + WCHAR filter_name[128]; + + HRESULT STDMETHODCALLTYPE GetClassID(CLSID* pClassID) override + { + debug("IPersist GetClassID"); + *pClassID = {}; + return E_UNEXPECTED; + } + + HRESULT STDMETHODCALLTYPE GetState(DWORD dwMilliSecsTimeout, FILTER_STATE* State) override + { + //debug("IMediaFilter GetState %lx", state); + *State = state; + return S_OK; + } + HRESULT STDMETHODCALLTYPE GetSyncSource(IReferenceClock** pClock) override + { + debug("IMediaFilter GetSyncSource"); + *pClock = nullptr; + return S_OK; + } + HRESULT STDMETHODCALLTYPE Pause() override + { + debug("IMediaFilter Pause"); + state = State_Paused; + return S_OK; + } + HRESULT STDMETHODCALLTYPE Run(REFERENCE_TIME tStart) override + { + debug("IMediaFilter Run"); + state = State_Running; + return S_OK; + } + HRESULT STDMETHODCALLTYPE SetSyncSource(IReferenceClock* pClock) override + { + debug("IMediaFilter SetSyncSource"); + return S_OK; + } + HRESULT STDMETHODCALLTYPE Stop() override + { + debug("IMediaFilter Stop"); + state = State_Stopped; + return S_OK; + } + + IEnumPins* enum_pins() requires requires { sizeof(parent()->pins); } + { + return new com_enum(parent()->pins, sizeof(parent()->pins) / sizeof(parent()->pins[0])); + } + HRESULT STDMETHODCALLTYPE EnumPins(IEnumPins** ppEnum) override + { + debug("IBaseFilter EnumPins"); + *ppEnum = parent()->enum_pins(); + return S_OK; + } + HRESULT STDMETHODCALLTYPE FindPin(LPCWSTR Id, IPin** ppPin) override + { + debug("IBaseFilter FindPin"); + *ppPin = nullptr; + return VFW_E_NOT_FOUND; + } + HRESULT STDMETHODCALLTYPE JoinFilterGraph(IFilterGraph* pGraph, LPCWSTR pName) override + { + debug("IBaseFilter JoinFilterGraph"); + if (pName) + wcscpy(filter_name, pName); + graph = pGraph; + return S_OK; + } + HRESULT STDMETHODCALLTYPE QueryFilterInfo(FILTER_INFO* pInfo) override + { + debug("IBaseFilter QueryFilterInfo"); + wcscpy(pInfo->achName, filter_name); + pInfo->pGraph = graph; + graph->AddRef(); + return S_OK; + } + HRESULT STDMETHODCALLTYPE QueryVendorInfo(LPWSTR* pVendorInfo) override + { + debug("IBaseFilter QueryVendorInfo"); + return E_NOTIMPL; + } +}; + +template +class base_pin : public Tbase { +public: + IPin* peer = nullptr; + + void debug(const char * fmt, ...) + { + return; + char buf[1024]; + + va_list args; + va_start(args, fmt); + vsnprintf(buf, 1024, fmt, args); + va_end(args); + +#ifdef __cpp_rtti + const char * my_typename = typeid(Touter).name(); +#else + const char * my_typename = ""; +#endif + fprintf(stdout, "plm %lu %s %s\n", GetCurrentThreadId(), my_typename, buf); + } + + Touter* parent() + { + return (Touter*)this; + } + + ULONG STDMETHODCALLTYPE AddRef() override { return parent()->parent()->AddRef(); } + ULONG STDMETHODCALLTYPE Release() override { return parent()->parent()->Release(); } + + HRESULT STDMETHODCALLTYPE BeginFlush() override + { + debug("IPin BeginFlush"); + if (is_output) + return E_UNEXPECTED; + return E_OUTOFMEMORY; + } + HRESULT STDMETHODCALLTYPE Connect(IPin* pReceivePin, const AM_MEDIA_TYPE * pmt) override + { + debug("IPin Connect %p %p %d", pReceivePin, pmt, is_output); + if constexpr (is_output) + { + if (true) // TODO: this is debug code, delete it + { + CComPtr p; + debug("X=%.8lx", pReceivePin->EnumMediaTypes(&p)); + if (p) + { + AM_MEDIA_TYPE* pmt; + while (p->Next(1, &pmt, nullptr) == S_OK) + { + debug("CANCONNECT %p %p %p\n", guid_to_str(pmt->majortype), guid_to_str(pmt->subtype), guid_to_str(pmt->formattype)); + } + } + } + if (parent()->connect_output(pReceivePin)) + { + debug("OUTPUT PIN CONNECTED"); + peer = pReceivePin; + return S_OK; + } + return VFW_E_NO_ACCEPTABLE_TYPES; + } + else return E_UNEXPECTED; + } + HRESULT STDMETHODCALLTYPE ConnectedTo(IPin** pPin) override + { + debug("IPin ConnectedTo %p %p", pPin, peer); + if (!peer) + return VFW_E_NOT_CONNECTED; + *pPin = peer; + peer->AddRef(); + return S_OK; + } + HRESULT STDMETHODCALLTYPE ConnectionMediaType(AM_MEDIA_TYPE* pmt) override + { + debug("IPin ConnectionMediaType"); + return E_OUTOFMEMORY; + } + HRESULT STDMETHODCALLTYPE Disconnect() override + { + debug("IPin Disconnect"); + peer = nullptr; + return S_OK; + } + HRESULT STDMETHODCALLTYPE EndFlush() override + { + if (is_output) + return E_UNEXPECTED; + debug("IPin EndFlush"); + return E_OUTOFMEMORY; + } + HRESULT STDMETHODCALLTYPE EndOfStream() override + { + debug("IPin EndOfStream"); + if constexpr (!is_output) + { + parent()->end_of_stream(); + return S_OK; + } + return E_UNEXPECTED; + } + HRESULT STDMETHODCALLTYPE EnumMediaTypes(IEnumMediaTypes** ppEnum) override + { + debug("IPin EnumMediaTypes"); + *ppEnum = nullptr; + return E_OUTOFMEMORY; + } + HRESULT STDMETHODCALLTYPE NewSegment(REFERENCE_TIME tStart, REFERENCE_TIME tStop, double dRate) override + { + debug("IPin NewSegment"); + return S_OK; + } + HRESULT STDMETHODCALLTYPE QueryAccept(const AM_MEDIA_TYPE * pmt) override + { + debug("IPin QueryAccept"); + return E_OUTOFMEMORY; + } + HRESULT STDMETHODCALLTYPE QueryDirection(PIN_DIRECTION* pPinDir) override + { + debug("IPin QueryDirection"); + *pPinDir = is_output ? PINDIR_OUTPUT : PINDIR_INPUT; + return S_OK; + } + HRESULT STDMETHODCALLTYPE QueryId(LPWSTR* Id) override + { + debug("IPin QueryId"); + return E_OUTOFMEMORY; + } + HRESULT STDMETHODCALLTYPE QueryInternalConnections(IPin** apPin, ULONG* nPin) override + { + debug("IPin QueryInternalConnections"); + return E_NOTIMPL; + } + HRESULT STDMETHODCALLTYPE QueryPinInfo(PIN_INFO* pInfo) override + { + debug("IPin QueryPinInfo"); + pInfo->pFilter = parent()->parent(); + pInfo->pFilter->AddRef(); + pInfo->dir = is_output ? PINDIR_OUTPUT : PINDIR_INPUT; + wcscpy(pInfo->achName, is_output ? L"source" : L"sink"); + return S_OK; + } + HRESULT STDMETHODCALLTYPE ReceiveConnection(IPin* pConnector, const AM_MEDIA_TYPE * pmt) override + { + debug("IPin ReceiveConnection %s %s %s", guid_to_str(pmt->majortype), guid_to_str(pmt->subtype), guid_to_str(pmt->formattype)); + if constexpr (!is_output) + { + if (parent()->connect_input(pConnector, pmt)) + { + debug("INPUT PIN CONNECTED"); + peer = pConnector; + return S_OK; + } + return VFW_E_TYPE_NOT_ACCEPTED; + } + else return E_UNEXPECTED; + } + + // IMemInputPin + HRESULT STDMETHODCALLTYPE GetAllocator(IMemAllocator** ppAllocator) + { + debug("IMemInputPin GetAllocator"); + return VFW_E_NO_ALLOCATOR; + } + HRESULT STDMETHODCALLTYPE GetAllocatorRequirements(ALLOCATOR_PROPERTIES* pProps) + { + debug("IMemInputPin GetAllocatorRequirements"); + return E_NOTIMPL; + } + HRESULT STDMETHODCALLTYPE NotifyAllocator(IMemAllocator* pAllocator, BOOL bReadOnly) + { + debug("IMemInputPin NotifyAllocator, readonly=%u", bReadOnly); + return S_OK; + } + HRESULT STDMETHODCALLTYPE Receive(IMediaSample* pSample) + { + BYTE* ptr; + pSample->GetPointer(&ptr); + size_t size = pSample->GetActualDataLength(); + //debug("IMemInputPin Receive %u", (unsigned)size); + parent()->receive_input(ptr, size); + return S_OK; + } + HRESULT STDMETHODCALLTYPE ReceiveCanBlock() + { + debug("IMemInputPin ReceiveCanBlock"); + return E_OUTOFMEMORY; + } + HRESULT STDMETHODCALLTYPE ReceiveMultiple(IMediaSample** pSamples, long nSamples, long* nSamplesProcessed) + { + debug("IMemInputPin ReceiveMultiple"); + return E_OUTOFMEMORY; + } + + void send_packet(IMediaSample* samp, const uint8_t * ptr, size_t len) + { + uint8_t* ptr2; + samp->GetPointer(&ptr2); + samp->SetActualDataLength(len); + memcpy(ptr2, ptr, len); + + CComPtr peer_mem; + peer->QueryInterface(IID_PPV_ARGS(&peer_mem)); + peer_mem->Receive(samp); + } +}; + +static void sample_set_time(IMediaSample* samp, double pts, double duration) +{ + if (pts != -1.0) + { + REFERENCE_TIME t = pts * 10000000; + if (duration != 0.0) + { + REFERENCE_TIME t2 = t + duration*10000000; + samp->SetTime(&t, &t2); + samp->SetMediaTime(&t, &t2); + } + else + { + samp->SetTime(&t, nullptr); + samp->SetMediaTime(&t, nullptr); + } + } + else + { + samp->SetTime(nullptr, nullptr); + samp->SetMediaTime(nullptr, nullptr); + } +} + +// Given the start of an MP2 packet, returns some information about this packet. +// Returns 1 if ok, 0 if incomplete packet, -1 if corrupt packet. +#define MP2_PACKET_MAX_SIZE 1728 +static inline int mp2_packet_parse(const uint8_t * ptr, size_t len, int* samplerate, int* n_channels, size_t* size) +{ + //size_t prefix_padding = 0; + //while (len > 0 && *ptr == 0x00) + //{ + //prefix_padding++; + //ptr++; + //len--; + //} + + //header is 48 bits + //11 bits sync, 0x7ff + //2 bits version, must be 3 + //2 bits layer, must be 2 + //1 bit hasCRC, can be whatever + + //4 bits bitrate index + //2 bits sample rate index + //1 bit padding flag + //1 bit private (ignore) + + //2 bits mode (stereo, joint stereo, dual channel, mono) + //2 bits mode extension (used only for joint stereo) + //4 bits copyright crap + + //16 bits checksum, if hasCRC + + if (len < 6) + return 0; + if (ptr[0] != 0xFF || (ptr[1]&0xFE) != 0xFC) + return -1; + + static const uint16_t bitrates[16] = { 0xFFFF, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 0xFFFF }; + static const uint16_t samplerates[4] = { 44100, 48000, 32000, 0xFFFF }; + + int bitrate = bitrates[(ptr[2]&0xF0)>>4]; + int sample_rate = samplerates[(ptr[2]&0x0C)>>2]; + if (bitrate == 0xFFFF || sample_rate == 0xFFFF) + return -1; + + bool has_pad = (ptr[2]&0x02); + + if (samplerate) + *samplerate = sample_rate; + if (n_channels) + *n_channels = ((ptr[3]&0xC0)>>6 == 3) ? 1 : 2; + if (size) + *size = (144000 * bitrate / sample_rate) + has_pad; + return 1; +} + +class CMPEG1Splitter : public base_filter { +public: + SRWLOCK srw; // threading: safe + CONDITION_VARIABLE wake_parent; // threading: safe + CONDITION_VARIABLE wake_video; // threading: safe + CONDITION_VARIABLE wake_audio; // threading: safe + bool have_video = false; // threading: parent only + bool have_audio = false; // threading: parent only + plm_demux_t* demux_video = nullptr; // threading: lock + plm_demux_t* demux_audio = nullptr; // threading: lock + + bool video_thread_exists = false; // threading: lock + bool video_thread_stop = false; // threading: lock + bool video_thread_active = false; // threading: lock; tells whether the child is currently writing anything + bool audio_thread_exists = false; // threading: lock + bool audio_thread_stop = false; // threading: lock + bool audio_thread_active = false; // threading: lock; tells whether the child is currently writing anything + + // FILTER_STATE state; // (from parent) threading: lock + + ~CMPEG1Splitter() + { + scoped_lock lock(&this->srw); + video_thread_stop = true; + audio_thread_stop = true; + WakeConditionVariable(&this->wake_video); + WakeConditionVariable(&this->wake_audio); + while (video_thread_exists || audio_thread_exists) + SleepConditionVariableSRW(&this->wake_parent, &this->srw, INFINITE, 0); + if (demux_video) + plm_demux_destroy(demux_video); + if (demux_audio) + plm_demux_destroy(demux_audio); + } + + class in_pin : public base_pin, in_pin> { + public: + CMPEG1Splitter* parent() { return container_of<&CMPEG1Splitter::pin_i>(this); } + + bool connect_input(IPin* pConnector, const AM_MEDIA_TYPE * pmt) + { + if (pmt->majortype == MEDIATYPE_Stream && pmt->subtype == MEDIASUBTYPE_MPEG1System) + { + CComPtr ar; + if (FAILED(pConnector->QueryInterface(&ar))) + return false; + + parent()->start_threads(ar); + return true; + } + + return false; + } + + void end_of_stream() + { + abort(); // we should get input only from IAsyncStream::SyncRead, EndOfStream() should be unreachable + } + }; + class out_pin_v : public base_pin, out_pin_v> { + public: + CMPEG1Splitter* parent() { return container_of<&CMPEG1Splitter::pin_v>(this); } + + MPEG1VIDEOINFO mvi_mediatype; + AM_MEDIA_TYPE am_mediatype; + + AM_MEDIA_TYPE* media_type() + { + int width = parent()->video_width; + int height = parent()->video_height; + double fps = parent()->video_fps; + + // why are some of those structs so obnoxiously deeply nested + mvi_mediatype = { + .hdr = { + .rcSource = { 0, 0, width, height }, + .rcTarget = { 0, 0, width, height }, + .dwBitRate = 0, + .dwBitErrorRate = 0, + .AvgTimePerFrame = (REFERENCE_TIME)(10000000/fps), + .bmiHeader = { + .biSize = sizeof(BITMAPINFOHEADER), + .biWidth = width, + .biHeight = height, + .biPlanes = 0, + .biBitCount = 0, + .biCompression = 0, + .biSizeImage = 0, + }, + }, + .dwStartTimeCode = 0, + .cbSequenceHeader = 0, + .bSequenceHeader = {}, + }; + am_mediatype = { + .majortype = MEDIATYPE_Video, + .subtype = MEDIASUBTYPE_MPEG1Packet, + .bFixedSizeSamples = false, + .bTemporalCompression = true, + .lSampleSize = 0, + .formattype = FORMAT_MPEGVideo, + .pUnk = nullptr, + .cbFormat = sizeof(mvi_mediatype), + .pbFormat = (BYTE*)&mvi_mediatype, + }; + + return &am_mediatype; + } + + bool connect_output(IPin* pReceivePin) + { + if (!parent()->have_video) + return false; + return SUCCEEDED(pReceivePin->ReceiveConnection(this, media_type())); + } + }; + class out_pin_a : public base_pin, out_pin_a> { + public: + CMPEG1Splitter* parent() { return container_of<&CMPEG1Splitter::pin_a>(this); } + + MPEG1WAVEFORMAT mwf_mediatype; + AM_MEDIA_TYPE am_mediatype; + + AM_MEDIA_TYPE* media_type() + { + int rate = parent()->audio_rate; + + mwf_mediatype = { + .wfx = { + .wFormatTag = WAVE_FORMAT_MPEG, + .nChannels = 2, + .nSamplesPerSec = (DWORD)rate, + .nAvgBytesPerSec = 4000, + .nBlockAlign = 48, + .wBitsPerSample = 0, + .cbSize = sizeof(mwf_mediatype), + }, + .fwHeadLayer = ACM_MPEG_LAYER2, + .dwHeadBitrate = (DWORD)rate, + .fwHeadMode = ACM_MPEG_STEREO, + .fwHeadModeExt = 0, + .wHeadEmphasis = 0, + .fwHeadFlags = ACM_MPEG_ID_MPEG1, + .dwPTSLow = 0, + .dwPTSHigh = 0, + }; + am_mediatype = { + .majortype = MEDIATYPE_Audio, + .subtype = MEDIASUBTYPE_MPEG1AudioPayload, // wine understands only MPEG1AudioPayload, not MPEG1Packet or MPEG1Payload + .bFixedSizeSamples = false, + .bTemporalCompression = true, + .lSampleSize = 0, + .formattype = FORMAT_WaveFormatEx, + .pUnk = nullptr, + .cbFormat = sizeof(mwf_mediatype), + .pbFormat = (BYTE*)&mwf_mediatype, + }; + return &am_mediatype; + } + + bool connect_output(IPin* pReceivePin) + { + if (!parent()->have_audio) + return false; + + return SUCCEEDED(pReceivePin->ReceiveConnection(this, media_type())); + } + }; + in_pin pin_i; + out_pin_v pin_v; // threading: parent only for most variables, read only while child exists for .peer + out_pin_a pin_a; // threading: parent only for most variables, read only while child exists for .peer + + IPin* pins[3] = { &pin_i, &pin_v, &pin_a }; + + int video_width; + int video_height; + double video_fps; + int audio_rate; + + HRESULT STDMETHODCALLTYPE Run(REFERENCE_TIME tStart) override + { + debug("IMediaFilter Run but better"); + scoped_lock lock(&this->srw); + state = State_Running; + WakeConditionVariable(&this->wake_video); + WakeConditionVariable(&this->wake_audio); + return S_OK; + } + HRESULT STDMETHODCALLTYPE Pause() override + { + debug("IMediaFilter Pause but better"); + scoped_lock lock(&this->srw); + state = State_Paused; + WakeConditionVariable(&this->wake_video); + WakeConditionVariable(&this->wake_audio); + return S_OK; + } + HRESULT STDMETHODCALLTYPE Stop() override + { + debug("IMediaFilter Stop but better"); + scoped_lock lock(&this->srw); + state = State_Stopped; + while (video_thread_active || audio_thread_active) + SleepConditionVariableSRW(&this->wake_parent, &this->srw, INFINITE, 0); + return S_OK; + } + + void start_threads(IAsyncReader* ar) + { + scoped_lock lock(&this->srw); + + // don't bother with async reading, just take the easiest solution + int64_t ignore; + int64_t size; + ar->Length(&ignore, &size); + uint8_t * ptr = (uint8_t*)malloc(size); + ar->SyncRead(0, size, ptr); + + // just tell one to free it, and the other not + demux_video = plm_demux_create(plm_buffer_create_with_memory(ptr, size, true), true); + demux_audio = plm_demux_create(plm_buffer_create_with_memory(ptr, size, false), true); + video_thread_active = false; + audio_thread_active = false; + + // - all pins must be connected before any filter can move to Paused + // - video decoder must know its output size before its pin can connect + // - video decoder won't have any input data before moving to Paused + // therefore, demuxer must provide this information to the decoder through another mechanism + // therefore, demuxer must START UP AN ENTIRE DECODER to read the headers and extract the video size + bool need_video = (plm_demux_get_num_video_streams(demux_video) > 0); + bool need_audio = (plm_demux_get_num_audio_streams(demux_video) > 0); + plm_buffer_t* video_buffer = plm_buffer_create_with_capacity(32768); + plm_video_t* video_checker = plm_video_create_with_buffer(video_buffer, false); + plm_buffer_t* audio_buffer = plm_buffer_create_with_capacity(32768); + plm_audio_t* audio_checker = plm_audio_create_with_buffer(audio_buffer, false); + + while (need_video || need_audio) + { + plm_packet_t* pack = plm_demux_decode(demux_video); + if (!pack) + break; + if (pack->type == PLM_DEMUX_PACKET_VIDEO_1 && need_video) + { + plm_buffer_write(video_buffer, pack->data, pack->length); + if (plm_video_has_header(video_checker)) + { + this->video_width = plm_video_get_width(video_checker); + this->video_height = plm_video_get_height(video_checker); + this->video_fps = plm_video_get_framerate(video_checker); + have_video = true; + need_video = false; + } + } + if (pack->type == PLM_DEMUX_PACKET_AUDIO_1 && need_audio) + { + plm_buffer_write(audio_buffer, pack->data, pack->length); + if (plm_audio_has_header(audio_checker)) + { + this->audio_rate = plm_audio_get_samplerate(audio_checker); + have_audio = true; + need_audio = false; + } + } + } + + plm_buffer_destroy(video_buffer); + plm_video_destroy(video_checker); + plm_buffer_destroy(audio_buffer); + plm_audio_destroy(audio_checker); + + plm_demux_rewind(demux_video); + + // need two threads, or the audio pin blocks while the video pin demands more input + // and I need two plm_demuxers, so they can go arbitrarily far out of sync + CreateThread(nullptr, 0, &CMPEG1Splitter::video_thread_fn_wrap, this, 0, nullptr); + CreateThread(nullptr, 0, &CMPEG1Splitter::audio_thread_fn_wrap, this, 0, nullptr); + } + + static DWORD WINAPI video_thread_fn_wrap(void* lpParameter) + { + ((CMPEG1Splitter*)lpParameter)->video_thread_fn(); + return 0; + } + void video_thread_fn() + { +//printf("VIDEOTHREAD=%.8lx\n", GetCurrentThreadId()); + scoped_lock lock(&this->srw); + bool first_packet = true; + + CComPtr mem_alloc; + mem_alloc.CoCreateInstance(CLSID_MemoryAllocator, NULL, CLSCTX_INPROC); + ALLOCATOR_PROPERTIES props = { 1, 65536, 1, 0 }; + mem_alloc->SetProperties(&props, &props); + mem_alloc->Commit(); + + while (!this->video_thread_stop) + { +//puts("vt1."); + while (this->state == State_Stopped) + { +//puts("vt2."); + if (this->video_thread_stop) + goto stop; +//puts("vt3."); + if (video_thread_active) + { +//puts("vt4."); + video_thread_active = false; + WakeConditionVariable(&this->wake_parent); + } +//puts("vt5."); + SleepConditionVariableSRW(&this->wake_video, &this->srw, INFINITE, 0); +//puts("vt6."); + } +//puts("vt7."); + video_thread_active = true; + plm_packet_t* pack = plm_demux_decode(demux_video); + if (!pack) + { + scoped_unlock unlock(&this->srw); + if (pin_v.peer) + pin_v.peer->EndOfStream(); + break; + } + if (pack->type == PLM_DEMUX_PACKET_VIDEO_1 && pin_v.peer) + { + scoped_unlock unlock(&this->srw); + + CComPtr samp; + mem_alloc->GetBuffer(&samp, nullptr, nullptr, 0); + + samp->SetDiscontinuity(first_packet); + samp->SetPreroll(false); + samp->SetSyncPoint(false); + sample_set_time(samp, pack->pts, 1.0 / video_fps); + pin_v.send_packet(samp, pack->data, pack->length); + + first_packet = false; + } + } + + stop: +//puts("vt999."); + this->video_thread_active = false; + this->video_thread_exists = false; + WakeConditionVariable(&this->wake_parent); + } + + static DWORD WINAPI audio_thread_fn_wrap(void* lpParameter) + { + ((CMPEG1Splitter*)lpParameter)->audio_thread_fn(); + return 0; + } + void audio_thread_fn() + { +//printf("AUDIOTHREAD=%.8lx\n", GetCurrentThreadId()); + scoped_lock lock(&this->srw); + bool first_packet = true; + + CComPtr mem_alloc; + mem_alloc.CoCreateInstance(CLSID_MemoryAllocator, NULL, CLSCTX_INPROC); + ALLOCATOR_PROPERTIES props = { 16, MP2_PACKET_MAX_SIZE, 1, 0 }; + mem_alloc->SetProperties(&props, &props); + mem_alloc->Commit(); + + size_t audio_chunk_pos = 0; + uint8_t audio_chunk[MP2_PACKET_MAX_SIZE]; + + while (!this->audio_thread_stop) + { +//puts("at1."); + while (this->state == State_Stopped) + { +//puts("at2."); + if (this->audio_thread_stop) + goto stop; +//puts("at3."); + if (audio_thread_active) + { +//puts("at4."); + audio_thread_active = false; + WakeConditionVariable(&this->wake_parent); + } +//puts("at5."); + SleepConditionVariableSRW(&this->wake_audio, &this->srw, INFINITE, 0); +//puts("at6."); + } +//puts("at7."); + audio_thread_active = true; + plm_packet_t* pack = plm_demux_decode(demux_audio); + if (!pack) + { + scoped_unlock unlock(&this->srw); + if (pin_a.peer) + pin_a.peer->EndOfStream(); + break; + } + if (pack->type == PLM_DEMUX_PACKET_AUDIO_1 && pin_a.peer) + { + scoped_unlock unlock(&this->srw); + + const uint8_t * new_buf = pack->data; + size_t new_len = pack->length; + + while (new_len) + { + size_t claim = min(new_len, MP2_PACKET_MAX_SIZE - audio_chunk_pos); + memcpy(audio_chunk + audio_chunk_pos, new_buf, claim); + audio_chunk_pos += claim; + new_buf += claim; + new_len -= claim; + + size_t pack_size = SIZE_MAX; + if (mp2_packet_parse(audio_chunk, audio_chunk_pos, NULL, NULL, &pack_size) < 0) + { + audio_chunk_pos = 0; // just discard it and see what happens + break; + } + if (pack_size <= audio_chunk_pos) + { + CComPtr samp; + mem_alloc->GetBuffer(&samp, nullptr, nullptr, 0); + + samp->SetDiscontinuity(first_packet); + samp->SetPreroll(false); + samp->SetSyncPoint(false); + sample_set_time(samp, pack->pts, 0.0); + pin_a.send_packet(samp, audio_chunk, pack_size); + + first_packet = false; + + memmove(audio_chunk, audio_chunk+pack_size, audio_chunk_pos-pack_size); + audio_chunk_pos -= pack_size; + } + } + } + } + + stop: + this->audio_thread_active = false; + this->audio_thread_exists = false; + WakeConditionVariable(&this->wake_parent); + } + + HRESULT STDMETHODCALLTYPE Count(DWORD* pcStreams) override + { + *pcStreams = have_video + have_audio; + return S_OK; + } + HRESULT STDMETHODCALLTYPE Enable(long lIndex, DWORD dwFlags) override + { + return S_OK; // just don't bother + } + HRESULT STDMETHODCALLTYPE Info(long lIndex, AM_MEDIA_TYPE** ppmt, DWORD* pdwFlags, LCID* plcid, + DWORD* pdwGroup, LPWSTR* ppszName, IUnknown** ppObject, IUnknown** ppUnk) override + { + bool is_video = (have_video && lIndex == 0); + if (have_video) lIndex--; + bool is_audio = (have_audio && lIndex == 0); + if (have_audio) lIndex--; + + if (!is_video && !is_audio) + return S_FALSE; + + if (ppmt) + { + if (is_video) + *ppmt = CreateMediaType(pin_v.media_type()); + if (is_audio) + *ppmt = CreateMediaType(pin_a.media_type()); + } + if (pdwFlags) + *pdwFlags = AMSTREAMSELECTINFO_ENABLED; // just don't bother + if (pdwGroup) + *pdwGroup = is_audio; + // ignore the others, Kirikiri just leaves them as null (and it never uses the group, it just stores it somewhere) + return S_OK; + } +}; + +class CMpegVideoCodec : public base_filter { +public: + plm_buffer_t* buf; + plm_video_t* decode; + + CMpegVideoCodec() + { + buf = plm_buffer_create_with_capacity(PLM_BUFFER_DEFAULT_SIZE); + decode = plm_video_create_with_buffer(buf, false); + } + ~CMpegVideoCodec() + { + plm_buffer_destroy(buf); + plm_video_destroy(decode); + } + + class in_pin : public base_pin, in_pin> { + public: + CMpegVideoCodec* parent() { return container_of<&CMpegVideoCodec::pin_i>(this); } + + bool connect_input(IPin* pConnector, const AM_MEDIA_TYPE * pmt) + { + if (pmt->majortype == MEDIATYPE_Video && pmt->subtype == MEDIASUBTYPE_MPEG1Packet) + return parent()->update_size_from_mediatype(pmt); + return false; + } + + void receive_input(uint8_t* ptr, size_t size) + { + plm_buffer_write(parent()->buf, ptr, size); + + while (true) + { + plm_frame_t* frame = plm_video_decode(parent()->decode); + if (!frame) + break; + + parent()->dispatch_frame(frame); + } + } + + void end_of_stream() + { + parent()->pin_o.peer->EndOfStream(); + } + }; + class out_pin : public base_pin, out_pin> { + public: + CMpegVideoCodec* parent() { return container_of<&CMpegVideoCodec::pin_o>(this); } + + bool connect_output(IPin* pReceivePin) + { + if (FAILED(pReceivePin->ReceiveConnection(this, parent()->media_type(MEDIASUBTYPE_YV12))) && + FAILED(pReceivePin->ReceiveConnection(this, parent()->media_type(MEDIASUBTYPE_RGB24))) && + FAILED(pReceivePin->ReceiveConnection(this, parent()->media_type(MEDIASUBTYPE_RGB32)))) + { + return false; + } + + parent()->commit_media_type(); + + return true; + } + }; + in_pin pin_i; + out_pin pin_o; + + IPin* pins[2] = { &pin_i, &pin_o }; + + CComPtr mem_alloc; + + VIDEOINFOHEADER vih_mediatype = { + .rcSource = { 0, 0, -1, -1 }, + .rcTarget = { 0, 0, -1, -1 }, + .dwBitRate = 0, + .dwBitErrorRate = 0, + .AvgTimePerFrame = -1, + .bmiHeader = { + .biSize = sizeof(BITMAPINFOHEADER), + .biWidth = -1, + .biHeight = -1, + .biPlanes = 1, + .biBitCount = 0, + .biCompression = 0, + .biSizeImage = 0xFFFFFFFF, + }, + }; + AM_MEDIA_TYPE am_mediatype = { + .majortype = MEDIATYPE_Video, + .subtype = {}, + .bFixedSizeSamples = false, + .bTemporalCompression = true, + .lSampleSize = 0, + .formattype = FORMAT_VideoInfo, + .pUnk = nullptr, + .cbFormat = sizeof(vih_mediatype), + .pbFormat = (BYTE*)&vih_mediatype, + }; + + bool update_size_from_mediatype(const AM_MEDIA_TYPE * pmt) + { + if (pmt->formattype != FORMAT_MPEGVideo) + return false; + + const MPEG1VIDEOINFO * vi = (MPEG1VIDEOINFO*)pmt->pbFormat; + vih_mediatype.rcSource = vi->hdr.rcSource; + vih_mediatype.rcTarget = vi->hdr.rcTarget; + vih_mediatype.AvgTimePerFrame = vi->hdr.AvgTimePerFrame; + vih_mediatype.bmiHeader.biWidth = vi->hdr.bmiHeader.biWidth; + vih_mediatype.bmiHeader.biHeight = vi->hdr.bmiHeader.biHeight; + return true; + } + + static void update_mediatype_from_subtype(AM_MEDIA_TYPE* pmt) + { + VIDEOINFOHEADER* vi = (VIDEOINFOHEADER*)pmt->pbFormat; + BITMAPINFOHEADER* bmi = &vi->bmiHeader; + if (pmt->subtype == MEDIASUBTYPE_YV12) + { + bmi->biBitCount = 12; + bmi->biCompression = MAKEFOURCC('Y','V','1','2'); + } + if (pmt->subtype == MEDIASUBTYPE_RGB24) + { + bmi->biBitCount = 24; + bmi->biCompression = BI_RGB; + } + if (pmt->subtype == MEDIASUBTYPE_RGB32) + { + bmi->biBitCount = 32; + bmi->biCompression = BI_RGB; + } + bmi->biSizeImage = bmi->biWidth * bmi->biHeight * bmi->biBitCount / 8; + } + + AM_MEDIA_TYPE* media_type(GUID subtype) + { + am_mediatype.subtype = subtype; + update_mediatype_from_subtype(&am_mediatype); + return &am_mediatype; + } + + void commit_media_type() + { + mem_alloc.CoCreateInstance(CLSID_MemoryAllocator, NULL, CLSCTX_INPROC); + ALLOCATOR_PROPERTIES props = { 1, (int32_t)vih_mediatype.bmiHeader.biSizeImage, 1024, 0 }; + mem_alloc->SetProperties(&props, &props); + mem_alloc->Commit(); + } + + void dispatch_frame(plm_frame_t* frame) + { + CComPtr samp; + mem_alloc->GetBuffer(&samp, nullptr, nullptr, 0); + + samp->SetDiscontinuity(false); + samp->SetPreroll(false); + samp->SetSyncPoint(true); + sample_set_time(samp, frame->time, 1.0 / plm_video_get_framerate(decode)); + + uint8_t* ptr2; + samp->GetPointer(&ptr2); + samp->SetActualDataLength(frame->width * frame->height * vih_mediatype.bmiHeader.biBitCount / 8); + + if (vih_mediatype.bmiHeader.biBitCount == 12) + { + for (size_t y=0;yheight;y++) + { + memcpy(ptr2, frame->y.data + frame->y.width*y, frame->width); + ptr2 += frame->width; + } + for (size_t y=0;yheight/2;y++) + { + memcpy(ptr2, frame->cr.data + frame->cr.width*y, frame->width/2); + ptr2 += frame->width/2; + } + for (size_t y=0;yheight/2;y++) + { + memcpy(ptr2, frame->cb.data + frame->cb.width*y, frame->width/2); + ptr2 += frame->width/2; + } + } + if (vih_mediatype.bmiHeader.biBitCount == 24) + { + plm_frame_to_bgr(frame, ptr2+(frame->height-1)*frame->width*3, -frame->width*3); + } + if (vih_mediatype.bmiHeader.biBitCount == 32) + { + plm_frame_to_bgra(frame, ptr2+(frame->height-1)*frame->width*4, -frame->width*4); + } + + CComPtr peer_mem; + pin_o.peer->QueryInterface(IID_PPV_ARGS(&peer_mem)); + peer_mem->Receive(samp); + } +}; + + + +// This class mostly represents functionality that exists in Wine, but works around a few Wine bugs. +// - IVMRSurfaceAllocatorNotify9::ChangeD3DDevice - semi-stub +// once this one is fixed, delete members need_reinit, every the_*, and everything that uses them +// - IVMRSurfaceAllocatorNotify9::NotifyEvent - stub +// once this one is fixed, simply delete the E_NOTIMPL check from NotifyEvent +// - Direct3D 9 can't draw on child windows +// once this one is fixed, delete function is_window_visible, member parent_window, and everything that uses them +// The rest of the class is simply boilerplate to inject my hacks where I need them. +class CVideoMixingRenderer9 : public com_base { + static bool is_window_visible(HWND hwnd) + { + DWORD wnd_pid; + DWORD my_pid = GetProcessId(GetCurrentProcess()); + GetWindowThreadProcessId(hwnd, &wnd_pid); + if (wnd_pid != my_pid) + return false; + + if (!IsWindowVisible(hwnd)) + return false; + + RECT rect; + GetClientRect(hwnd, &rect); + // I don't know what a visible 0x0 window means, but they exist sometimes + if (rect.right == 0 || rect.bottom == 0) + return false; + + return true; + } + static BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam) + { + HWND* ret = (HWND*)lParam; + + if (!is_window_visible(hwnd)) + return TRUE; + if (*ret) + return FALSE; + *ret = hwnd; + return TRUE; + } + static HWND find_the_only_visible_window() + { + HWND ret = nullptr; + if (!EnumWindows(EnumWindowsProc, (LPARAM)&ret)) + return nullptr; + return ret; + } + + class fancy_surfalloc : public IVMRSurfaceAllocator9, IVMRImagePresenter9 { + public: + IVMRSurfaceAllocator9* parent_alloc; + IVMRImagePresenter9* parent_pres; + + bool need_reinit = false; + IDirect3DDevice9* the_d3ddevice; + IDirect3DSurface9** the_surface; + DWORD the_surfaceflags; + DWORD_PTR the_userid; + VMR9AllocationInfo the_allocinfo; + + bool need_move_window; + //HWND parent_window = nullptr; + //DWORD parent_orig_style; + + //CVideoMixingRenderer9* parent() { return container_of<&CVideoMixingRenderer9::surfalloc_wrapper>(this); } + DWORD STDMETHODCALLTYPE AddRef() { return parent_alloc->AddRef(); } + DWORD STDMETHODCALLTYPE Release() { return parent_alloc->Release(); } + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override + { +//printf("QI(surfalloc)=%s\n", guid_to_str(riid)); + *ppvObject = nullptr; + if (riid == __uuidof(IVMRImagePresenter9)) + { + *ppvObject = (void*)(IVMRImagePresenter9*)this; + this->AddRef(); + return S_OK; + } + return parent_alloc->QueryInterface(riid, ppvObject); + } + + HRESULT STDMETHODCALLTYPE AdviseNotify(IVMRSurfaceAllocatorNotify9* lpIVMRSurfAllocNotify) + { return parent_alloc->AdviseNotify(lpIVMRSurfAllocNotify); } + HRESULT STDMETHODCALLTYPE GetSurface(DWORD_PTR dwUserID, DWORD SurfaceIndex, DWORD SurfaceFlags, IDirect3DSurface9** lplpSurface) + { + the_surface = lplpSurface; + the_surfaceflags = SurfaceFlags; + return parent_alloc->GetSurface(dwUserID, SurfaceIndex, SurfaceFlags, lplpSurface); + } + HRESULT STDMETHODCALLTYPE InitializeDevice(DWORD_PTR dwUserID, VMR9AllocationInfo* lpAllocInfo, DWORD* lpNumBuffers) + { + need_reinit = false; + need_move_window = true; + // DXVK doesn't have this glitch + // I'd rather detect wined3d, not DXVK, but those are the only two options, so good enough + // (native windows shouldn't run this code at all) + CComPtr detect_dxvk; + if (SUCCEEDED(the_d3ddevice->QueryInterface(IID_ID3D9VkInteropDevice, (void**)&detect_dxvk))) + need_move_window = false; + the_userid = dwUserID; + the_allocinfo = *lpAllocInfo; + return parent_alloc->InitializeDevice(dwUserID, lpAllocInfo, lpNumBuffers); + } + HRESULT STDMETHODCALLTYPE TerminateDevice(DWORD_PTR dwID) + { + return parent_alloc->TerminateDevice(dwID); + } + + + HRESULT STDMETHODCALLTYPE PresentImage(DWORD_PTR dwUserID, VMR9PresentationInfo* lpPresInfo) + { + if (need_move_window) + { + need_move_window = false; + + HWND parent = find_the_only_visible_window(); + HWND child = GetWindow(parent, GW_CHILD); + while (child) + { + char buf[256]; + GetClassName(child, buf, 256); + if (!strcmp(buf, "krmovie VMR9 Child Window Class")) + { + RECT rect; + GetClientRect(child, &rect); + POINT pt = { 0, 0 }; + ClientToScreen(parent, &pt); + + // need to hide the window before setting the parent + // don't know if windows limitation, wine limitation, x11 limitation, window manager limitation, or whatever + // don't really care either + ShowWindow(child, SW_HIDE); + + // WS_POPUP seems to do pretty much nothing in this year (other than mess with WS_CHILD), + // but it seems intended to be set on parented borderless windows like this, so let's do it + SetWindowLong(child, GWL_STYLE, (GetWindowLong(child, GWL_STYLE) & ~WS_CHILD) | WS_POPUP); + SetParent(child, nullptr); + // no clue why owner is set by setting PARENT, but that's what it is + SetWindowLongPtr(child, GWLP_HWNDPARENT, (LONG_PTR)parent); + + MoveWindow(child, pt.x, pt.y, rect.right, rect.bottom, FALSE); + + ShowWindow(child, SW_SHOW); + + //break; + } + child = GetWindow(child, GW_HWNDNEXT); + } + } + return parent_pres->PresentImage(dwUserID, lpPresInfo); + } + HRESULT STDMETHODCALLTYPE StartPresenting(DWORD_PTR dwUserID) + { + return parent_pres->StartPresenting(dwUserID); + } + HRESULT STDMETHODCALLTYPE StopPresenting(DWORD_PTR dwUserID) + { + return parent_pres->StopPresenting(dwUserID); + } + + void evil_reinit() + { + if (!need_reinit) + return; + + need_reinit = false; + the_surface[0]->Release(); + the_surface[0] = nullptr; + this->TerminateDevice(the_userid); + DWORD one = 1; + this->InitializeDevice(the_userid, &the_allocinfo, &one); + this->GetSurface(the_userid, 0, the_surfaceflags, the_surface); + } + }; + + class fancy_san9 : public IVMRSurfaceAllocatorNotify9 { + public: + IVMRSurfaceAllocatorNotify9* parent; + + CVideoMixingRenderer9* container() { return container_of<&CVideoMixingRenderer9::san_wrapper>(this); } + + DWORD STDMETHODCALLTYPE AddRef() { return parent->AddRef(); } + DWORD STDMETHODCALLTYPE Release() { return parent->Release(); } + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override + { +//printf("QI(san)=%s\n", guid_to_str(riid)); + return parent->QueryInterface(riid, ppvObject); + } + + HRESULT STDMETHODCALLTYPE AdviseSurfaceAllocator(DWORD_PTR dwUserID, IVMRSurfaceAllocator9* lpIVRMSurfaceAllocator) + { + //puts("FSAN9 AdviseSurfaceAllocator"); + container()->surfalloc_wrapper.parent_alloc = lpIVRMSurfaceAllocator; + lpIVRMSurfaceAllocator->QueryInterface(&container()->surfalloc_wrapper.parent_pres); + container()->surfalloc_wrapper.parent_pres->Release(); + return parent->AdviseSurfaceAllocator(dwUserID, &container()->surfalloc_wrapper); + } + HRESULT STDMETHODCALLTYPE AllocateSurfaceHelper(VMR9AllocationInfo* lpAllocInfo, DWORD* lpNumBuffers, IDirect3DSurface9** lplpSurface) + { + //puts("FSAN9 AllocateSurfaceHelper"); +//DWORD gg=*lpNumBuffers; +HRESULT hr=parent->AllocateSurfaceHelper(lpAllocInfo, lpNumBuffers, lplpSurface); +//printf("FSAN9 AllocateSurfaceHelper %lu->%lu %.8lx\n", gg,*lpNumBuffers,hr); +return hr; + + return parent->AllocateSurfaceHelper(lpAllocInfo, lpNumBuffers, lplpSurface); + } + HRESULT STDMETHODCALLTYPE ChangeD3DDevice(IDirect3DDevice9* lpD3DDevice, HMONITOR hMonitor) + { + //puts("FSAN9 ChangeD3DDevice"); + container()->surfalloc_wrapper.need_reinit = true; + container()->surfalloc_wrapper.the_d3ddevice = lpD3DDevice; + HRESULT hr = parent->ChangeD3DDevice(lpD3DDevice, hMonitor); + container()->surfalloc_wrapper.evil_reinit(); + return hr; + } + HRESULT STDMETHODCALLTYPE NotifyEvent(LONG EventCode, LONG_PTR Param1, LONG_PTR Param2) + { + //puts("FSAN9 NotifyEvent"); + HRESULT hr = parent->NotifyEvent(EventCode, Param1, Param2); + if (hr == E_NOTIMPL) + hr = container()->graph_mes->Notify(EventCode, Param1, Param2); + return hr; + } + HRESULT STDMETHODCALLTYPE SetD3DDevice(IDirect3DDevice9* lpD3DDevice, HMONITOR hMonitor) + { + //puts("FSAN9 SetD3DDevice"); + container()->surfalloc_wrapper.the_d3ddevice = lpD3DDevice; + return parent->SetD3DDevice(lpD3DDevice, hMonitor); + } + }; + +public: + CComPtr parent; + CComPtr parent_bmp; + IMediaEventSink* graph_mes; + + fancy_surfalloc surfalloc_wrapper; + fancy_san9 san_wrapper; + + CVideoMixingRenderer9() + { +//puts("CREATE VMR9"); + auto* pDllGetClassObject = (decltype(DllGetClassObject)*)GetProcAddress(GetModuleHandle("quartz.dll"), "DllGetClassObject"); + CComPtr fac; + pDllGetClassObject(CLSID_VideoMixingRenderer9, IID_IClassFactory, (void**)&fac); + fac->CreateInstance(nullptr, IID_IBaseFilter, (void**)&parent); + parent->QueryInterface(&parent_bmp); + } + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override + { +//printf("QI(vmr9)=%s\n", guid_to_str(riid)); + if (riid == __uuidof(IBaseFilter)) + { + *ppvObject = (void*)(IBaseFilter*)this; + this->AddRef(); + return S_OK; + } + if (riid == __uuidof(IVMRSurfaceAllocatorNotify9)) + { + parent->QueryInterface(&san_wrapper.parent); + // don't hold this ref, it's a ref cycle + // I don't think COM objects are allowed to have different refcount for different interfaces, but Windows and Wine both do this + san_wrapper.parent->Release(); + *ppvObject = (void*)(IVMRSurfaceAllocatorNotify9*)&san_wrapper; + san_wrapper.AddRef(); + return S_OK; + } + return parent->QueryInterface(riid, ppvObject); + } + + HRESULT STDMETHODCALLTYPE GetClassID(CLSID* pClassID) override + { return parent->GetClassID(pClassID); } + HRESULT STDMETHODCALLTYPE GetState(DWORD dwMilliSecsTimeout, FILTER_STATE* State) override + { return parent->GetState(dwMilliSecsTimeout, State); } + HRESULT STDMETHODCALLTYPE GetSyncSource(IReferenceClock** pClock) override + { return parent->GetSyncSource(pClock); } + HRESULT STDMETHODCALLTYPE Pause() override + { return parent->Pause(); } + HRESULT STDMETHODCALLTYPE Run(REFERENCE_TIME tStart) override + { return parent->Run(tStart); } + HRESULT STDMETHODCALLTYPE SetSyncSource(IReferenceClock* pClock) override + { return parent->SetSyncSource(pClock); } + HRESULT STDMETHODCALLTYPE Stop() override + { return parent->Stop(); } + HRESULT STDMETHODCALLTYPE EnumPins(IEnumPins** ppEnum) override + { return parent->EnumPins(ppEnum); } + HRESULT STDMETHODCALLTYPE FindPin(LPCWSTR Id, IPin** ppPin) override + { return parent->FindPin(Id, ppPin); } + HRESULT STDMETHODCALLTYPE JoinFilterGraph(IFilterGraph* pGraph, LPCWSTR pName) override + { + graph_mes = nullptr; + if (pGraph) + { + pGraph->QueryInterface(&graph_mes); + graph_mes->Release(); // don't hold a reference to the graph, that's a ref cycle + } + return parent->JoinFilterGraph(pGraph, pName); + } + HRESULT STDMETHODCALLTYPE QueryFilterInfo(FILTER_INFO* pInfo) override + { return parent->QueryFilterInfo(pInfo); } + HRESULT STDMETHODCALLTYPE QueryVendorInfo(LPWSTR* pVendorInfo) override + { return parent->QueryVendorInfo(pVendorInfo); } +}; + + + +class fancy_WMSyncReader : public com_base { +public: + CComPtr parent; + CComPtr parent_prof; + + static const constexpr GUID expected_magic = { 0xe23d171f, 0x3df0, 0x4a57, 0x8d, 0xb5, 0x1b, 0xc3, 0x4c, 0xca, 0x97, 0x7a }; + struct sneaky_information { + fancy_WMSyncReader* self; + WORD streamnumber; + GUID real_subtype; + GUID magic; + }; + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override + { + if (riid == IID_IWMProfile) + { + this->AddRef(); + *ppvObject = (void*)(IWMProfile*)this; + return parent->QueryInterface(&parent_prof); + } + return parent->QueryInterface(riid, ppvObject); + } + + HRESULT STDMETHODCALLTYPE Close() override + { return parent->Close(); } + HRESULT STDMETHODCALLTYPE GetMaxOutputSampleSize(DWORD dwOutput, DWORD* pcbMax) override + { return parent->GetMaxOutputSampleSize(dwOutput, pcbMax); } + HRESULT STDMETHODCALLTYPE GetMaxStreamSampleSize(WORD wStream, DWORD* pcbMax) override + { return parent->GetMaxStreamSampleSize(wStream, pcbMax); } + HRESULT STDMETHODCALLTYPE GetNextSample(WORD wStreamNum, INSSBuffer** ppSample, QWORD* pcnsSampleTime, QWORD* pcnsDuration, + DWORD* pdwFlags, DWORD* pdwOutputNum, WORD* pwStreamNum) override + { + //printf("GETNEXTSAMPLE BEGIN %u\n", wStreamNum); + HRESULT hr = parent->GetNextSample(wStreamNum, ppSample, pcnsSampleTime, pcnsDuration, pdwFlags, pdwOutputNum, pwStreamNum); + //printf("GETNEXTSAMPLE END %.8lx\n", hr); + if (hr == VFW_E_NOT_COMMITTED) + { + // this seems to happen while the filter graph is flushing during a seek + // haven't investigated it very closely + return NS_E_NO_MORE_SAMPLES; + } + return hr; + } + HRESULT STDMETHODCALLTYPE GetOutputCount(DWORD* pcOutputs) override + { return parent->GetOutputCount(pcOutputs); } + HRESULT STDMETHODCALLTYPE GetOutputFormat(DWORD dwOutputNum, DWORD dwFormatNum, IWMOutputMediaProps** ppProps) override + { return parent->GetOutputFormat(dwOutputNum, dwFormatNum, ppProps); } + HRESULT STDMETHODCALLTYPE GetOutputFormatCount(DWORD dwOutputNum, DWORD* pcFormats) override + { return parent->GetOutputFormatCount(dwOutputNum, pcFormats); } + HRESULT STDMETHODCALLTYPE GetOutputNumberForStream(WORD wStreamNum, DWORD* pdwOutputNum) override + { return parent->GetOutputNumberForStream(wStreamNum, pdwOutputNum); } + HRESULT STDMETHODCALLTYPE GetOutputProps(DWORD dwOutputNum, IWMOutputMediaProps** ppOutput) override + { return parent->GetOutputProps(dwOutputNum, ppOutput); } + HRESULT STDMETHODCALLTYPE GetOutputSetting(DWORD dwOutputNum, LPCWSTR pszName, + WMT_ATTR_DATATYPE* pType, BYTE* pValue, WORD* pcbLength) override + { return parent->GetOutputSetting(dwOutputNum, pszName, pType, pValue, pcbLength); } + HRESULT STDMETHODCALLTYPE GetReadStreamSamples(WORD wStreamNum, BOOL* pfCompressed) override + { return parent->GetReadStreamSamples(wStreamNum, pfCompressed); } + HRESULT STDMETHODCALLTYPE GetStreamNumberForOutput(DWORD dwOutputNum, WORD* pwStreamNum) override + { return parent->GetStreamNumberForOutput(dwOutputNum, pwStreamNum); } + HRESULT STDMETHODCALLTYPE GetStreamSelected(WORD wStreamNum, WMT_STREAM_SELECTION* pSelection) override + { return parent->GetStreamSelected(wStreamNum, pSelection); } + HRESULT STDMETHODCALLTYPE Open(const WCHAR* pwszFilename) override + { return parent->Open(pwszFilename); } + HRESULT STDMETHODCALLTYPE OpenStream(IStream* pStream) override + { return parent->OpenStream(pStream); } + HRESULT STDMETHODCALLTYPE SetOutputProps(DWORD dwOutputNum, IWMOutputMediaProps* pOutput) override + { return parent->SetOutputProps(dwOutputNum, pOutput); } + HRESULT STDMETHODCALLTYPE SetOutputSetting(DWORD dwOutputNum, LPCWSTR pszName, + WMT_ATTR_DATATYPE Type, const BYTE* pValue, WORD cbLength) override + { return parent->SetOutputSetting(dwOutputNum, pszName, Type, pValue, cbLength); } + HRESULT STDMETHODCALLTYPE SetRange(QWORD cnsStartTime, LONGLONG cnsDuration) override + { return parent->SetRange(cnsStartTime, cnsDuration); } + HRESULT STDMETHODCALLTYPE SetRangeByFrame(WORD wStreamNum, QWORD qwFrameNumber, LONGLONG cFramesToRead) override + { return parent->SetRangeByFrame(wStreamNum, qwFrameNumber, cFramesToRead); } + HRESULT STDMETHODCALLTYPE SetReadStreamSamples(WORD wStreamNum, BOOL fCompressed) override + { return parent->SetReadStreamSamples(wStreamNum, fCompressed); } + HRESULT STDMETHODCALLTYPE SetStreamsSelected(WORD cStreamCount, WORD* pwStreamNumbers, WMT_STREAM_SELECTION* pSelections) override + { return parent->SetStreamsSelected(cStreamCount, pwStreamNumbers, pSelections); } + + class fancy_WMStreamConfig : public com_base { + public: + fancy_WMSyncReader* owner; + + CComPtr parent; + CComPtr parent_props; + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override + { + if (riid == IID_IWMMediaProps) + { + this->AddRef(); + *ppvObject = (void*)(IWMMediaProps*)this; + return parent->QueryInterface(&parent_props); + } + return parent->QueryInterface(riid, ppvObject); + } + + HRESULT STDMETHODCALLTYPE GetBitrate(DWORD* pdwBitrate) override + { return parent->GetBitrate(pdwBitrate); } + HRESULT STDMETHODCALLTYPE GetBufferWindow(DWORD* pmsBufferWindow) override + { return parent->GetBufferWindow(pmsBufferWindow); } + HRESULT STDMETHODCALLTYPE GetConnectionName(WCHAR* pwszInputName, WORD* pcchInputName) override + { return parent->GetConnectionName(pwszInputName, pcchInputName); } + HRESULT STDMETHODCALLTYPE GetStreamName(WCHAR* pwszStreamName, WORD* pcchStreamName) override + { return parent->GetStreamName(pwszStreamName, pcchStreamName); } + HRESULT STDMETHODCALLTYPE GetStreamNumber(WORD* pwStreamNum) override + { return parent->GetStreamNumber(pwStreamNum); } + HRESULT STDMETHODCALLTYPE GetStreamType(GUID* pguidStreamType) override + { return parent->GetStreamType(pguidStreamType); } + HRESULT STDMETHODCALLTYPE SetBitrate(DWORD pdwBitrate) override + { return parent->SetBitrate(pdwBitrate); } + HRESULT STDMETHODCALLTYPE SetBufferWindow(DWORD msBufferWindow) override + { return parent->SetBufferWindow(msBufferWindow); } + HRESULT STDMETHODCALLTYPE SetConnectionName(LPCWSTR pwszInputName) override + { return parent->SetConnectionName(pwszInputName); } + HRESULT STDMETHODCALLTYPE SetStreamName(LPCWSTR pwszStreamName) override + { return parent->SetStreamName(pwszStreamName); } + HRESULT STDMETHODCALLTYPE SetStreamNumber(WORD wStreamNum) override + { return parent->SetStreamNumber(wStreamNum); } + + HRESULT STDMETHODCALLTYPE GetMediaType(WM_MEDIA_TYPE* pType, DWORD* pcbType) override + { + HRESULT hr = parent_props->GetMediaType(pType, pcbType); + // subtype must be one of + /* + if( type == WMMEDIASUBTYPE_MP43 || type == WMMEDIASUBTYPE_MP4S || type == WMMEDIASUBTYPE_MPEG2_VIDEO || + type == WMMEDIASUBTYPE_MSS1 || type == WMMEDIASUBTYPE_MSS2 || type == WMMEDIASUBTYPE_WMVP || + type == WMMEDIASUBTYPE_WMAudio_Lossless || type == WMMEDIASUBTYPE_WMAudioV2 || type == WMMEDIASUBTYPE_WMAudioV7 || + type == WMMEDIASUBTYPE_WMAudioV8 || type == WMMEDIASUBTYPE_WMAudioV9 || type == WMMEDIASUBTYPE_WMSP1 || + type == WMMEDIASUBTYPE_WMV1 || type == WMMEDIASUBTYPE_WMV2 || type == WMMEDIASUBTYPE_WMV3 ) + */ + // let's just take the first video and first audio format from the above + + // must also sneak in the real subtype somewhere + *pcbType += sizeof(sneaky_information); + + if (pType) + { + uint8_t* after_end = (uint8_t*)pType->pbFormat + pType->cbFormat; + sneaky_information sneak = { owner, 0, pType->subtype, expected_magic }; + parent->GetStreamNumber(&sneak.streamnumber); + memcpy(after_end, &sneak, sizeof(sneaky_information)); + pType->cbFormat += sizeof(sneaky_information); + + if (pType->majortype == MEDIATYPE_Video) + pType->subtype = WMMEDIASUBTYPE_MP43; // originally MEDIASUBTYPE_RGB24 + if (pType->majortype == MEDIATYPE_Audio) + pType->subtype = WMMEDIASUBTYPE_WMAudio_Lossless; // originally WMMEDIASUBTYPE_PCM + } + return hr; + } + HRESULT STDMETHODCALLTYPE GetType(GUID* pguidType) override + { return parent_props->GetType(pguidType); } + HRESULT STDMETHODCALLTYPE SetMediaType(WM_MEDIA_TYPE* pType) override + { return parent_props->SetMediaType(pType); } + }; + + HRESULT STDMETHODCALLTYPE AddMutualExclusion(IWMMutualExclusion* pME) override + { return parent_prof->AddMutualExclusion(pME); } + HRESULT STDMETHODCALLTYPE AddStream(IWMStreamConfig* pConfig) override + { return parent_prof->AddStream(pConfig); } + HRESULT STDMETHODCALLTYPE CreateNewMutualExclusion(IWMMutualExclusion** ppME) override + { return parent_prof->CreateNewMutualExclusion(ppME); } + HRESULT STDMETHODCALLTYPE CreateNewStream(REFGUID guidStreamType, IWMStreamConfig** ppConfig) override + { return parent_prof->CreateNewStream(guidStreamType, ppConfig); } + HRESULT STDMETHODCALLTYPE GetDescription(WCHAR* pwszDescription, DWORD* pcchDescription) override + { return parent_prof->GetDescription(pwszDescription, pcchDescription); } + HRESULT STDMETHODCALLTYPE GetMutualExclusion(DWORD dwMEIndex, IWMMutualExclusion** ppME) override + { return parent_prof->GetMutualExclusion(dwMEIndex, ppME); } + HRESULT STDMETHODCALLTYPE GetMutualExclusionCount(DWORD* pcMutexs) override + { return parent_prof->GetMutualExclusionCount(pcMutexs); } + HRESULT STDMETHODCALLTYPE GetName(WCHAR* pwszName, DWORD* pcchName) override + { return parent_prof->GetName(pwszName, pcchName); } + HRESULT STDMETHODCALLTYPE GetStream(DWORD dwStreamIndex, IWMStreamConfig** ppConfig) override + { return parent_prof->GetStream(dwStreamIndex, ppConfig); } + HRESULT STDMETHODCALLTYPE GetStreamByNumber(WORD wStreamNumber, IWMStreamConfig** ppIStream) override + { + fancy_WMStreamConfig* ret = new fancy_WMStreamConfig(); + ret->owner = this; + *ppIStream = ret; + return parent_prof->GetStreamByNumber(wStreamNumber, &ret->parent); + } + HRESULT STDMETHODCALLTYPE GetStreamCount(DWORD* pcStreams) override + { return parent_prof->GetStreamCount(pcStreams); } + HRESULT STDMETHODCALLTYPE GetVersion(WMT_VERSION* pdwVersion) override + { return parent_prof->GetVersion(pdwVersion); } + HRESULT STDMETHODCALLTYPE ReconfigStream(IWMStreamConfig* pConfig) override + { return parent_prof->ReconfigStream(pConfig); } + HRESULT STDMETHODCALLTYPE RemoveMutualExclusion(IWMMutualExclusion* pME) override + { return parent_prof->RemoveMutualExclusion(pME); } + HRESULT STDMETHODCALLTYPE RemoveStream(IWMStreamConfig* pConfig) override + { return parent_prof->RemoveStream(pConfig); } + HRESULT STDMETHODCALLTYPE RemoveStreamByNumber(WORD wStreamNum) override + { return parent_prof->RemoveStreamByNumber(wStreamNum); } + HRESULT STDMETHODCALLTYPE SetDescription(const WCHAR* pwszDescription) override + { return parent_prof->SetDescription(pwszDescription); } + HRESULT STDMETHODCALLTYPE SetName(const WCHAR* pwszName) override + { return parent_prof->SetName(pwszName); } + + void set_subtype(DWORD dwOutputNum, GUID subtype) + { + DWORD idx; + parent->GetOutputNumberForStream(dwOutputNum, &idx); + CComPtr prop; + parent->GetOutputProps(idx, &prop); + char buf[256]; // needs 160 (or 90 for the audio channel), let's give it a little more + WM_MEDIA_TYPE* mt = (WM_MEDIA_TYPE*)buf; + DWORD n = sizeof(buf); + prop->GetMediaType(mt, &n); + if (mt->majortype == MEDIATYPE_Video && mt->subtype != subtype) + { + mt->subtype = subtype; + prop->SetMediaType(mt); + parent->SetOutputProps(idx, prop); + } + } +}; + +class fake_DMOObject final : public IMediaObject { +public: + class own_iunknown_t : public IUnknown { + public: + ULONG refcount = 1; + fake_DMOObject* parent() { return container_of<&fake_DMOObject::own_iunknown>(this); } + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override + { + return parent()->NonDelegatingQueryInterface(riid, ppvObject); + } + + ULONG STDMETHODCALLTYPE AddRef() override + { + return ++refcount; + } + ULONG STDMETHODCALLTYPE Release() override + { + uint32_t new_refcount = --refcount; + if (!new_refcount) + delete parent(); + return new_refcount; + } + }; + own_iunknown_t own_iunknown; + IUnknown* parent; + + AM_MEDIA_TYPE mt; + BYTE mt_buf[128]; + bool convert_rgb24_rgb32 = false; + + CComPtr my_buffer; + DWORD buf_dwFlags; + REFERENCE_TIME buf_rtTimestamp; + REFERENCE_TIME buf_rtTimelength; + + fancy_WMSyncReader* predecessor; + WORD streamnumber; + + fake_DMOObject(IUnknown* outer) : parent(outer) {} + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override + { return parent->QueryInterface(riid, ppvObject); } + ULONG STDMETHODCALLTYPE AddRef() override + { return parent->AddRef(); } + ULONG STDMETHODCALLTYPE Release() override + { return parent->Release(); } + + HRESULT NonDelegatingQueryInterface(REFIID riid, void** ppvObject) + { + if (riid == IID_IMediaObject) + { + parent->AddRef(); + *ppvObject = (void*)(IMediaObject*)this; + return S_OK; + } + return E_NOINTERFACE; + } + + HRESULT STDMETHODCALLTYPE AllocateStreamingResources() override + { return S_OK; } + HRESULT STDMETHODCALLTYPE Discontinuity(DWORD dwInputStreamIndex) override + { return S_OK; } + HRESULT STDMETHODCALLTYPE Flush() override + { + my_buffer = nullptr; + return S_OK; + } + HRESULT STDMETHODCALLTYPE FreeStreamingResources() override + { return S_OK; } + HRESULT STDMETHODCALLTYPE GetInputCurrentType(DWORD dwInputStreamIndex, DMO_MEDIA_TYPE* pmt) override + { return E_OUTOFMEMORY; } + HRESULT STDMETHODCALLTYPE GetInputMaxLatency(DWORD dwInputStreamIndex, REFERENCE_TIME* prtMaxLatency) override + { return E_OUTOFMEMORY; } + HRESULT STDMETHODCALLTYPE GetInputSizeInfo(DWORD dwInputStreamIndex, DWORD* pcbSize, + DWORD* pcbMaxLookahead, DWORD* pcbAlignment) override + { return E_OUTOFMEMORY; } + HRESULT STDMETHODCALLTYPE GetInputStatus(DWORD dwInputStreamIndex, DWORD* dwFlags) override + { return E_OUTOFMEMORY; } + HRESULT STDMETHODCALLTYPE GetInputStreamInfo(DWORD dwInputStreamIndex, DWORD* pdwFlags) override + { return E_OUTOFMEMORY; } + HRESULT STDMETHODCALLTYPE GetInputType(DWORD dwInputStreamIndex, DWORD dwTypeIndex, DMO_MEDIA_TYPE* pmt) override + { return E_OUTOFMEMORY; } + HRESULT STDMETHODCALLTYPE GetOutputCurrentType(DWORD dwOutputStreamIndex, DMO_MEDIA_TYPE* pmt) override + { return E_OUTOFMEMORY; } + HRESULT STDMETHODCALLTYPE GetOutputSizeInfo(DWORD dwOutputStreamIndex, DWORD* pcbSize, DWORD* pcbAlignment) override + { + if (dwOutputStreamIndex != 0) + return DMO_E_INVALIDSTREAMINDEX; + if (mt.formattype == FORMAT_VideoInfo || mt.formattype == FORMAT_VideoInfo2) + { + VIDEOINFOHEADER* vi = (VIDEOINFOHEADER*)mt.pbFormat; + *pcbSize = vi->bmiHeader.biBitCount * vi->bmiHeader.biWidth * vi->bmiHeader.biHeight / 8; + *pcbAlignment = 1; // Kirikiri TBufferRendererAllocator::SetProperties accepts only 1, anything else returns VFW_E_BADALIGN + } + else if (mt.formattype == FORMAT_WaveFormatEx) + { + WAVEFORMATEX* wfx = (WAVEFORMATEX*)mt.pbFormat; + *pcbSize = wfx->nAvgBytesPerSec * 4; // usual buffer size is 1 second, but let's give it some more + *pcbAlignment = 8; // float32 * two channels, should be enough + } + else + { + *pcbSize = 1024*1024; // just pick something + *pcbAlignment = 8; + } +//printf("GETOUTPUTSIZEINFO %lu -> %lu\n", dwOutputStreamIndex, *pcbSize); + return S_OK; + } + HRESULT STDMETHODCALLTYPE GetOutputStreamInfo(DWORD dwOutputStreamIndex, DWORD* pdwFlags) override + { return E_OUTOFMEMORY; } + HRESULT STDMETHODCALLTYPE GetOutputType(DWORD dwOutputStreamIndex, DWORD dwTypeIndex, DMO_MEDIA_TYPE* pmt) override + { +//printf("get output type %lu\n", dwTypeIndex); + if (dwOutputStreamIndex != 0) + return DMO_E_INVALIDSTREAMINDEX; + if (dwTypeIndex >= 3 || (dwTypeIndex == 1 && mt.majortype != MEDIATYPE_Video)) + return DMO_E_NO_MORE_ITEMS; + CopyMediaType((AM_MEDIA_TYPE*)pmt, &mt); + if (pmt->majortype == MEDIATYPE_Video) + { + if (dwTypeIndex == 0) + { + pmt->subtype = MEDIASUBTYPE_YV12; + CMpegVideoCodec::update_mediatype_from_subtype((AM_MEDIA_TYPE*)pmt); + } + if (dwTypeIndex == 1) + { + pmt->subtype = MEDIASUBTYPE_RGB24; + CMpegVideoCodec::update_mediatype_from_subtype((AM_MEDIA_TYPE*)pmt); + } + if (dwTypeIndex == 2) + { + pmt->subtype = MEDIASUBTYPE_RGB32; + CMpegVideoCodec::update_mediatype_from_subtype((AM_MEDIA_TYPE*)pmt); + } + } + return S_OK; + } + HRESULT STDMETHODCALLTYPE GetStreamCount(DWORD* pcInputStreams, DWORD* pcOutputStreams) override + { + *pcInputStreams = 1; + *pcOutputStreams = 1; + return S_OK; + } + HRESULT STDMETHODCALLTYPE Lock(LONG bLock) override + { return E_OUTOFMEMORY; } + HRESULT STDMETHODCALLTYPE ProcessInput(DWORD dwInputStreamIndex, IMediaBuffer* pBuffer, DWORD dwFlags, + REFERENCE_TIME rtTimestamp, REFERENCE_TIME rtTimelength) override + { +//printf("ProcessInput %p\n", this); + if (my_buffer) + return E_OUTOFMEMORY; + my_buffer = pBuffer; + buf_dwFlags = dwFlags; + buf_rtTimestamp = rtTimestamp; + buf_rtTimelength = rtTimelength; + return S_OK; + } + HRESULT STDMETHODCALLTYPE ProcessOutput(DWORD dwFlags, DWORD cOutputBufferCount, + DMO_OUTPUT_DATA_BUFFER* pOutputBuffers, DWORD* pdwStatus) override + { +//printf("ProcessOutput %p\n", this); + if (!my_buffer) + return S_FALSE; + if (cOutputBufferCount != 1) + return E_FAIL; + + BYTE* bytes; + DWORD len; + my_buffer->GetBufferAndLength(&bytes, &len); + if (convert_rgb24_rgb32) + { + pOutputBuffers[0].pBuffer->SetLength(len*4/3); + BYTE* bytes2; + DWORD len2; + pOutputBuffers[0].pBuffer->GetBufferAndLength(&bytes2, &len2); + convert_rgb24_to_rgb32(bytes2, bytes, len/3); + } + else + { + pOutputBuffers[0].pBuffer->SetLength(len); + BYTE* bytes2; + DWORD len2; + pOutputBuffers[0].pBuffer->GetBufferAndLength(&bytes2, &len2); + memcpy(bytes2, bytes, len2); + } + pOutputBuffers[0].dwStatus = buf_dwFlags & ~DMO_OUTPUT_DATA_BUFFERF_INCOMPLETE; + pOutputBuffers[0].rtTimestamp = buf_rtTimestamp; + pOutputBuffers[0].rtTimelength = buf_rtTimelength; + my_buffer = nullptr; + return S_OK; + } + HRESULT STDMETHODCALLTYPE SetInputMaxLatency(DWORD dwInputStreamIndex, REFERENCE_TIME rtMaxLatency) override + { return E_OUTOFMEMORY; } + HRESULT STDMETHODCALLTYPE SetInputType(DWORD dwInputStreamIndex, const DMO_MEDIA_TYPE* pmt, DWORD dwFlags) override + { + if (dwInputStreamIndex != 0) + return DMO_E_INVALIDSTREAMINDEX; + if (!pmt && (dwFlags & DMO_SET_TYPEF_CLEAR)) + return S_OK; + fancy_WMSyncReader::sneaky_information sneak; + uint8_t* after_end = (uint8_t*)pmt->pbFormat + pmt->cbFormat - sizeof(sneak); + memcpy(&sneak, after_end, sizeof(sneak)); + if (sneak.magic != fancy_WMSyncReader::expected_magic) + return DMO_E_TYPE_NOT_ACCEPTED; + if (!(dwFlags & DMO_SET_TYPEF_TEST_ONLY)) + { + mt = *(AM_MEDIA_TYPE*)pmt; + mt.subtype = sneak.real_subtype; + mt.cbFormat -= sizeof(sneak); + memcpy(mt_buf, mt.pbFormat, mt.cbFormat); + mt.pbFormat = mt_buf; + + predecessor = sneak.self; + streamnumber = sneak.streamnumber; + } + return S_OK; + } + HRESULT STDMETHODCALLTYPE SetOutputType(DWORD dwOutputStreamIndex, const DMO_MEDIA_TYPE* pmt, DWORD dwFlags) override + { + if (dwOutputStreamIndex != 0) + return DMO_E_INVALIDSTREAMINDEX; + if (dwFlags & DMO_SET_TYPEF_CLEAR) + return S_OK; +//if (pmt->subtype == MEDIASUBTYPE_YV12) + //printf("set subtype YV12 fl %lu\n", dwFlags); +//else if (pmt->subtype == MEDIASUBTYPE_RGB24) + //printf("set subtype RGB24 fl %lu\n", dwFlags); +//else if (pmt->subtype == MEDIASUBTYPE_RGB32) + //printf("set subtype RGB32 fl %lu\n", dwFlags); +//else if (pmt->subtype == WMMEDIASUBTYPE_PCM) + //printf("set subtype PCM fl %lu\n", dwFlags); +//else + //printf("set subtype %s fl %lu\n",guid_to_str(pmt->subtype), dwFlags); + if (pmt->subtype != mt.subtype && !(dwFlags & DMO_SET_TYPEF_TEST_ONLY)) + { + mt.subtype = pmt->subtype; + convert_rgb24_rgb32 = false; + if (mt.subtype == MEDIASUBTYPE_RGB32) + { + convert_rgb24_rgb32 = true; + predecessor->set_subtype(streamnumber, MEDIASUBTYPE_RGB24); + } + else + predecessor->set_subtype(streamnumber, mt.subtype); + CMpegVideoCodec::update_mediatype_from_subtype(&mt); + } + return S_OK; + } +}; + +template +class ClassFactory : public com_base { +public: + HRESULT STDMETHODCALLTYPE CreateInstance(IUnknown* pUnkOuter, REFIID riid, void** ppvObject) + { + if (pUnkOuter != nullptr) + return CLASS_E_NOAGGREGATION; + return qi_release(new T(), riid, ppvObject); + } + HRESULT STDMETHODCALLTYPE LockServer(BOOL lock) { return S_OK; } // don't care +}; + +template +class AggregatingClassFactory : public com_base { +public: + HRESULT STDMETHODCALLTYPE CreateInstance(IUnknown* pUnkOuter, REFIID riid, void** ppvObject) + { + if (pUnkOuter == nullptr) + return VFW_E_NEED_OWNER; // wrong if this isn't a VFW object, but who cares, it's an error, good enough + if (riid != IID_IUnknown) + return E_NOINTERFACE; + T* ret = new T(pUnkOuter); + *ppvObject = (void*)(IUnknown*)&ret->own_iunknown; + return S_OK; + } + HRESULT STDMETHODCALLTYPE LockServer(BOOL lock) { return S_OK; } // don't care +}; + +typedef HRESULT STDMETHODCALLTYPE (*WMCreateSyncReader_t)(IUnknown* pUnkCert, DWORD dwRights, IWMSyncReader** ppSyncReader); +static WMCreateSyncReader_t orig_WMCreateSyncReader; +static HRESULT STDMETHODCALLTYPE myWMCreateSyncReader(IUnknown* pUnkCert, DWORD dwRights, IWMSyncReader** ppSyncReader) +{ + fancy_WMSyncReader* ret = new fancy_WMSyncReader(); + *ppSyncReader = ret; + //puts("krkrwine: creating sync reader"); + HRESULT hr = orig_WMCreateSyncReader(pUnkCert, dwRights, &ret->parent); + //puts("krkrwine: created sync reader"); + return hr; +} +static FARPROC STDMETHODCALLTYPE myGetProcAddress(HMODULE hModule, LPCSTR lpProcName) +{ + if (!strcmp(lpProcName, "WMCreateSyncReader")) + { + orig_WMCreateSyncReader = (WMCreateSyncReader_t)GetProcAddress(hModule, "WMCreateSyncReader"); + return (FARPROC)myWMCreateSyncReader; + } + return GetProcAddress(hModule, lpProcName); +} + +static void very_unsafe_init() +{ + static bool initialized = false; + if (initialized) + return; + initialized = true; + + // refuse to operate on Windows + if (!GetProcAddress(GetModuleHandle("ntdll.dll"), "wine_get_version")) + return; + + DWORD ignore; + + IClassFactory* fac_mpegsplit = new ClassFactory(); + CoRegisterClassObject(CLSID_MPEG1Splitter, fac_mpegsplit, CLSCTX_INPROC_SERVER, REGCLS_MULTIPLEUSE, &ignore); + + IClassFactory* fac_mpegvideo = new ClassFactory(); + CoRegisterClassObject(CLSID_CMpegVideoCodec, fac_mpegvideo, CLSCTX_INPROC_SERVER, REGCLS_MULTIPLEUSE, &ignore); + + IClassFactory* fac_vmr9 = new ClassFactory(); + CoRegisterClassObject(CLSID_VideoMixingRenderer9, fac_vmr9, CLSCTX_INPROC_SERVER, REGCLS_MULTIPLEUSE, &ignore); + + // if all I needed was to swap/implement the above objects, I'd put that in install.py + // unfortunately, I also need to hijack WMCreateSyncReader, which is loaded via GetProcAddress + // I need some seriously deep shenanigans to be able to access that + + // _rmovie is just a debug aid + // use LoadLibrary, not GetProcAddress, so my hacks aren't undone if the DLL is unloaded and reloaded + HMODULE mod = LoadLibrary("_rmovie.dll"); + if (!mod) + mod = LoadLibrary("krmovie.dll"); + if (!mod) + return; + + uint8_t* base_addr = (uint8_t*)mod; + IMAGE_DOS_HEADER* head_dos = (IMAGE_DOS_HEADER*)base_addr; + IMAGE_NT_HEADERS* head_nt = (IMAGE_NT_HEADERS*)(base_addr + head_dos->e_lfanew); + IMAGE_DATA_DIRECTORY section_dir = head_nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; + IMAGE_IMPORT_DESCRIPTOR* imports = (IMAGE_IMPORT_DESCRIPTOR*)(base_addr + section_dir.VirtualAddress); + + void* find_function = (void*)GetProcAddress; + //void* find_function = (void*)GetProcAddress(GetModuleHandle("kernel32.dll"), "GetProcAddress"); // in case your compiler sucks + void* replace_function = (void*)myGetProcAddress; + + while (imports->Name) + { + void* * out = (void**)(base_addr + imports->FirstThunk); + while (*out) + { + if (*out == find_function) + { + // can't just *out = replace_function, import table is read only + WriteProcessMemory(GetCurrentProcess(), out, &replace_function, sizeof(replace_function), NULL); + } + out++; + } + imports++; + } + + // only register these if the fake WMCreateSyncReader can be injected + IClassFactory* fac_dmo = new AggregatingClassFactory(); + CoRegisterClassObject(CLSID_CWMVDecMediaObject, fac_dmo, CLSCTX_INPROC_SERVER, REGCLS_MULTIPLEUSE, &ignore); + CoRegisterClassObject(CLSID_CWMADecMediaObject, fac_dmo, CLSCTX_INPROC_SERVER, REGCLS_MULTIPLEUSE, &ignore); +} + +#ifdef __i386__ +// needs some extra shenanigans to kill the stdcall @12 suffix +#define EXPORT(ret, name, args) \ + __asm__(".section .drectve; .ascii \" -export:" #name "\"; .text"); \ + extern "C" __stdcall ret name args __asm__("_" #name); \ + extern "C" __stdcall ret name args +#else +#define EXPORT(ret, name, args) \ + extern "C" __attribute__((__visibility__("default"))) __stdcall ret name args; \ + extern "C" __attribute__((__visibility__("default"))) __stdcall ret name args +#endif + +EXPORT(HRESULT, DllGetClassObject, (REFCLSID rclsid, REFIID riid, void** ppvObj)) +{ + //freopen("Z:\\dev\\stdout", "wt", stdout); + //setvbuf(stdout, nullptr, _IONBF, 0); + //puts("krkrwine - hello world"); + + very_unsafe_init(); + + // this DllGetClassObject doesn't actually implement anything, it just calls very_unsafe_init then defers to the original + return ((decltype(DllGetClassObject)*)GetProcAddress(LoadLibrary("quartz.dll"), "DllGetClassObject"))(rclsid, riid, ppvObj); +} + +EXPORT(HRESULT, DllCanUnloadNow, ()) +{ + return S_FALSE; // just don't bother +} + +#ifdef __MINGW32__ +// deleting these things removes a few kilobytes of binary and a dependency on libstdc++-6.dll +void* operator new(std::size_t n) _GLIBCXX_THROW(std::bad_alloc) { return malloc(n); } +void operator delete(void* p) noexcept { free(p); } +void operator delete(void* p, std::size_t n) noexcept { operator delete(p); } +extern "C" void __cxa_pure_virtual() { __builtin_trap(); } +extern "C" void _pei386_runtime_relocator() {} +#endif diff --git a/making.md b/making.md new file mode 100644 index 0000000..64ca106 --- /dev/null +++ b/making.md @@ -0,0 +1,722 @@ +Making-of +========= + +(This is a mostly accurate summary of what I did to create this thing. At least the latter parts are; the former are somewhat unchronological, since I only started writing this until I was a week into the project.) + +Proton can, as of writing, not run certain Japanese visual novels properly; the videos fail to play. In some games, it just skips them; in some, the entire game crashes at that point. + +With Steam's Windows 7/8 deadline looming, I need another way to run them. (No, Windows 10 doesn't count; I've heard too many scary stories about telemetry, forced updates, and otherwise disrespecting or ignoring the user's workflow.) + +Googling it reveals some rumblings about the MPEG-1 video codec. It also reveals someone's put together a test VN, and even located the source code of Kirikiri, one of the most common Japanese VN engines (western VNs tend to prefer Ren'py). https://bugs.winehq.org/show_bug.cgi?id=9127#c102 + +MPEGs won't work, here's the source code of a BuildMPEGGraph function, let's just extract it to a separate project. Oh look, CLSID_CMpegVideoCodec isn't implemented in Wine. My googlings also revealed a quick and easy MPEG decoder, let's just hook it up. It's even split to demuxer / video decoder / audio decoder. https://github.com/phoboslab/pl_mpeg + +The first roadblock +------------------- + +(Spoiler: It's not the last one. The scrollbar size probably spoiled that already, anyways.) + +Okay, I've got a half-finished DirectShow object with an input pin, let's connect it and see which functions it calls... ...why doesn't it connect to my pin? + +Why does CLSID_MPEG1Splitter not have a video output pin? + +Guess it makes sense; there is, after all, no input pin in Wine that can accept that media type. Let's just reimplement that object too. + +...where did the sound go? + +The second roadblock +-------------------- + +Wine's multimedia codecs are outsourced to GStreamer; the DirectShow filters are just wrappers. CLSID_CMpegAudioCodec creates an avdec_mp2float internally. Let's implement pl_mpeg in GStreamer too; I'll need it when/if Wine gains proper support for the MPEG objects. (Luckily, Wine looks up GStreamer elements by media type, not name, so I can just install mine and it'll work.) + +How does GStreamer play an MPEG file? Let's pull up the log level, create a playbin element, and see what objects are inside... + +...that's playbin, uridecodebin, filesrc, decodebin, typefind, mpegpsdemux, multiqueue, mpegvideoparse, mpegaudioparse, xvimagesink, avdec_mpeg2video, pulsesink, pulsesink, avdec_mp2float, input-selector, input-selector, tee, bin, queue, identity, videobalance, videoconvert, videoscale, bin, videoconvert, deinterlace, bin, queue, identity, volume, audioconvert, and audioresample. That's a lot. + +I don't know what half of them are, but most of them seem irrelevant to me. Some additional experiments reveal that the necessary objects are + +gst-launch-1.0 filesrc location=video.mpg ! mpegpsdemux name=demux ! mpegvideoparse ! avdec_mpeg2video ! queue ! autovideosink \ + demux. ! mpegaudioparse ! avdec_mp2float ! audioconvert ! queue ! autoaudiosink + +What's this mpegaudioparse thing? It doesn't seem to do anything, it just takes bytes and returns the same bytes. Stacking multiple mpegaudioparse on each other does nothing. Maybe it adds media format information, for example sample rate? + +Turns out MPEG-1, for some reason, contains two different levels of framing. The outer layer separates video and audio, and contains timing information - but inside that is another byte stream. A random packet from the outer layer is unusable as is - the decoder must first identify an inner packet boundary, and then parse the packets one at the time. That's what mpegaudioparse does - it takes the bytes, and returns the same, in differently-sized chunks. + +Fortunately, MPEG-1 Audio Layer 2 framing is simple. Reading the pl_mpeg source code reveals the format, and writing my own parser and wiring it up to my demuxer is 50 lines of code. + +I had some minor roadblocks with GStreamer segment formats too, but they too quickly fell. + +Unfortunately, video parsing is a lot more complicated. Fortunately, my demuxer doesn't need to be connected to avdec_mpeg2video; it only needs to be connected to the matching pl_mpeg element, I don't need to match what Windows passes between the corresponding elements. + +Now let's compile this thing as 32bit too, so it runs in Wine. Many of my VNs are 32bit, and while the Wine developers are working on running 32bit EXEs in 64bit Linux processes, that part is far from done. So I need gcc-multilib, and ... why is it complaining about C++ headers? Ah well, it's almost valid C already, might as well convert it the entire way. And ... obviously I need the GStreamer libraries, which means I need their pkg-config, which took a while to google (the top results say pkg-config accepts a --host argument, but it doesn't. Instead, --help reveals that the correct incantation is --personality=i386-linux-gnu.) + +Let's try it in Proton too, via the CLSID_decodebin_parser object ...fails to load, can't find function gst_buffer_new_memdup. Ah well, that's easy to reimplement. Let's try again... wrong again, GStreamer version mismatch. Another easy fix, I don't need to report GST_VERSION_MAJOR and GST_VERSION_MINOR in GST_PLUGIN_DEFINE. And ... now it works. + +Well, that's one problem less, let's go back to the Windows side and finish those DirectShow objects. + +The third roadblock +------------------- + +COM objects require a lot of boilerplate. I'd like to template it away. For example, there's a bunch of near-identical enumerator interfaces; I'd like to have one template that implements them all, templated only on the interface, and how to duplicate the return value (defaulting to AddRef). Something like + +template(Ti* obj) { obj->AddRef(); return obj; }> +class ComEnumerator { + +Unfortunately, GCC is not quite pleased with that token soup, calling it an "internal compiler error". + +One quick workaround (and a GCC bugzilla search that returned https://gcc.gnu.org/bugzilla/show_bug.cgi?id=105667) later, back to the DirectShow thing... + +...so how do I fill in the AM_MEDIA_TYPE? + +The fourth roadblock +-------------------- + +The filters can't connect unless they know the media type, including image size. The video decoder doesn't have the image size; it doesn't even have the byte stream. + +Whatever, I'll just hardcode the video's size for now and worry about that later. Let's hope my recipient pin doesn't mind the YV12 pixel format. + +...so how is the data supposed to flow? Isn't the graph manager supposed to handle that? + +DirectShow data flow +-------------------- + +In GStreamer, the file source element pushes bytes to the sink as fast as the sink wants them. In DirectShow, the file source is just passive, the demuxer filter has to ask for bytes. I'm lazy, let's just read the entire thing into memory and not worry about it. + +So now the filters are going from stopped to paused... ...why aren't they going to running? + +Because data should flow through the filters even while paused. + +But when do I submit those bytes? The filters won't go to running until I've submitted some bytes, but the graph's thread is currently busy waiting for that other filter to start. Do I need to create my own thread? + +I don't like threading. It's powerful, but also dangerous. For IO-bound tasks like this, threads should not be used. + +But that's not how DirectShow is designed; I need to create a thread. In fact, I need to create two - one for audio, one for video. I don't like any part of this. I don't always agree with the GNOME team's GUI designers, but their APIs are almost always good. + +(In fact, it's even documented that I need to create threads. Naturally, I found that documentation only after wasting several hours reverse engineering what I'm supposed to do...) + +Well, data is finally flowing, and my homemade recipient pin just dumps the video frames to disk, so I don't care about pixel format. Let's see if I can deal with the hardcoded video size... + +...where is the 'change format' function? + +DirectShow media format negotiation +----------------------------------- + +There are a couple of available 'change format' functions, depending on how much of the format I want to change. https://learn.microsoft.com/en-us/windows/win32/directshow/dynamic-format-changes + +Unfortunately, Wine's VMR-9 renderer doesn't support any of them. Back to the drawing board... which parts are immutable facts, and which are just incorrect assumptions? How does Windows solve that? + +Trying to run my programs on a real Windows returns... several segfaults. I could debug it, but there are easier ways to figure out what it does. For example, I could simply check the documentation; the real MPEG demuxer uses the MPEG1VIDEOINFO format structure. Which contains size information. Three times. + +Which means the demuxer must know the video size. Which means I must start up the pl_mpeg video decoder in the demuxer object. Ah well, this thing isn't intended to be upstreamed, it's fine if it's messy. + +Well, that's the last piece. Let's rewrite some of Wine's registry keys so it picks my MPEG decoder objects, open this test VN, and enjoy the results. + +Of course not +------------- + +Nope, just the same black screen as before. Let's check the rest of Kirikiri's source code... + +...wow, that's a lot of weird stuff. Let's start with these logging and exception handling functions, I want to see what they're called with. They're implemented in the .exe and called from a .dll, I can hijack them. Let's just swap out this function pointer. + +Segfault, with an absurd stack trace. Of course. Why would anything ever be easy? + +Detours and detours +------------------- + +Let's disassemble that DLL. + +Nope, the function remains as bytes. Let's take those bytes and send to my grab-bag of random utility functions (one of which is an x86 disassembler)... + +Nope, 500 Internal Server Error. That is not the correct answer. Let's log in to the server and attach a debugger... + +Nope, can't compile the debug-friendly build because a completely different module is throwing warnings about misleading indentation. Yes, that program sucks, but... sigh, let's just fix that. What is this programming detour chain phenomenon called? It feels like the kind of thing that should have a name. + +Next up: Fails to compile because I'm passing wrong number of arguments to libbfd's init_disassemble_info. Must've changed during the Debian 12 upgrade. That's probably why it's crashing, too. + +One fix later, and ... that worked. Can't have bad luck every time, I guess. The answer is ret 4; this program has set the default calling convention to stdcall. How entertaining. Well, that's easy to fix. + +One detour less +--------------- + +...still segfaults. Note to self: If I hijack a 'get function address' function, swap in my own one, and keep the previous one around (for all those functions I don't want to implement myself), do not install that hijack twice, that'll just get recursive. + +One fix later, and the logger says ... "krmovie : Use offscreen and YUV surface."; no exceptions, nothing else. That doesn't tell me what the problem is, but it does tell me a whole bunch of things about what it isn't. + +Let's add some heavier logging... preferably by switching from hijacking memory addresses in DllGetClassObject to making a custom krmovie.dll that mostly delegates to the real one, but also adds whatever logging I want. It's easier, safer, and means I can inject my hijacks earlier. + +I also need to hijack what functions the real krmovie imports, so I can add logging on that boundary too. Easiest way to do that is grab some code from a previous project. https://github.com/Alcaro/plungor + +Let's start with replacing RegisterClassExA and CreateWindowA, and splitting out the VMR-9 window as a toplevel window, just to see what happens. + +I know it needs to call IDirect3DDevice9::Present(). That is called when IMediaEvent::GetEvent() gives the expected value. Said expected value is sent to IVMRSurfaceAllocatorNotify9::NotifyEvent()... let's see how Wine implements that... + +FIXME("(%p/%p)->(...) stub\n", iface, This); return E_NOTIMPL; + +Well, that's clear enough. Finally solved this mystery, I'll hijack its VMR-9 and implement that function myself. + +...wait, that function isn't even called. Why is that function not called? + +One detour more +--------------- + +Is it because CVMRCustomAllocatorPresenter9::PresentImage (in Kirikiri) is returning D3DERR_INVALIDCALL? Which function is returning that? The easiest way to figure out that would be setting WINEDEBUG=d3d9+trace, but my Wine installation seems to have trace information compiled out. Fun. + +The second easiest solution would be sticking a breakpoint on the call to that function and just stepping through it. Let's see how well gdb runs in Wine... + +...not very. It can't set breakpoints on code that isn't loaded yet, which my DLL obviously isn't. But DebugBreak() works well enough. + +And by 'well enough', I mean that trying to step over a call instruction, or out of the function, places the instruction pointer one byte too late. Ah well, easy enough to fix... would get annoying in long debug sessions, but in this case, it fails quite fast. It's in IDirect3DDevice9::SetRenderTarget, and checking with Wine's source code reveals that the chosen render target is placed on wrong device. + +Why is it creating two devices, why is it keeping both around, and which of the two is wrong? How many missing Wine features am I running into? How close am I to the goal? + +Does it have something to do with that second CreateWindow call I saw? + +A fourth missing Wine feature +----------------------------- + +Turns out Wine's VMR-9 doesn't implement IVMR9SurfaceAllocatorNotify::ChangeD3DDevice() properly. It's supposed to reinitialize the IVMRSurfaceAllocator9, but it doesn't, that'd be too easy. + +It has an initialization function called when connecting the DirectShow pin... but that, of course, happened long ago. And it's not called directly, it goes via some internal helper object. I can't reach that function. + +So... what if I do something really ugly? If I can't call that function, can I recreate it? + +The VMR-9 object doesn't hold many Direct3D resources, just a few surfaces. And while Wine has infrastructure in place to use multiple surfaces, it's unused in practice; it just uses one. + +And the pointer to that surface is passed out when connecting the DirectShow pin. What if... what if I just hold onto that pointer, and when the program asks to swap the Direct3D device, I swap it out? + +One last detour +--------------- + +Surprisingly, it works. And patching up NotifyEvent() is easy too. It's finally presenting images. + +Well, except that part where it isn't, of course. It is presenting the images, the Direct3D backbuffer has the right contents, but nothing shows up. + +And this time, gdb can't help me; it gets stuck in absurd locations whenever I try to set a breakpoint. My guess is Kirikiri contains a catch block or five, and they confuse gdb. + +Then let's go back to the oldest tricks in the book - shotgun and printf debugging. Let's just change things around at random. For example, let's put back the CreateWindow hijack (I removed it a while ago for no real reason). + +![image](making/finally.png) + +...wow, that worked. + +So, in conclusion... + +- CLSID_CMpegVideoCodec needed to be implemented +- CLSID_MPEG1Splitter needed to be reimplemented +- CLSID_VideoMixingRenderer9 had at least two missing pieces, which needed to be patched over +- Something about Direct3D 9 does not work in child windows + +The quest continues +------------------- + +Getting it to show the video in a separate window is great progress, but I'm far from done. I still need to + +- Investigate WMV/WMA support, and fix if necessary +- Implement it by replacing COM objects only, no custom krmovie.dll, so it can be installed into Wine and not into individual games; ideally also detect if the Wine bugs got patched, so my hijacks can hide themselves +- Get the same behavior as Windows, without a separate window +- Investigate shutdown behavior, I think there's some memory leaks and/or use-after-free +- Get it into Proton +- Investigate other VN engines; Kirikiri is the most common, but not the only one (though I suspect most of them suffer from the same bugs. Maybe they even copied code from each other.) +- Enjoy my hard-earned VNs + +Let's start with the third one, it's the most reliant on my current testing framework. Once I start on the others, I'll probably dismantle most of it. Fourth should be done alongside second. + +Extracting the relevant functionality into a separate program reveals that I can simply call IDirect3DDevice9::Present() with a different HWND, and it'll work. + +Doing that in Kirikiri reveals that it does, in fact, not work. Because why would anything ever be easy? + +How does that window differ from a normal parent window? How does it render? + +I can't even find where in the source code the window is created, it refers to a bunch of functions that don't exist. Googling them points to some strange Delphi runtime support library; weird to call that from C++, but not the worst I've seen, I guess... + +From what I can see, it doesn't import DirectDraw, OpenGL, nor any version of Direct3D (and Vulkan wasn't invented when it was created). Is it plain old GDI, is it drawing using some rare API I'm not familiar with, is it importing things in a way I don't recognize, or something else? It does import CreateDIBSection; the source code I have only shows that function in some bitmap overlay object, but that proves nothing, it could be in that mysterious Delphi runtime library. + +The answer was obvious +---------------------- + +The window had a child window already. I did successfully draw on the parent window, but this extra child covered everything. + +Obvious follow up question: What do I do with that information? + +Do I delete the child window? Do I move it offscreen? Do I hide it? Do I remove WS_CLIPCHILDREN from the parent and hope the child window doesn't do anything unhelpful? + +For testing, the easiest and safest solution is deleting it. In practice, I want the solution with the lowest risk of breaking if the Wine bug gets fixed, and in programs that use this object differently; this means it's better to remove WS_CLIPCHILDREN. + +Either way, that's the third task done. Now that I know how, it's time to move that functionality into a custom VideoMixingRenderer9 and remove my fake krmovie.dll. + +Naturally, this isn't smooth sailing either... + +The two missing pieces in VideoMixingRenderer9 are easy and straightforward. But the hole in Direct3D is less easy; I am given access to the Direct3D device, and I can redirect the Present calls to another window - but which window? I'm never given one, and I can't extract it from the Direct3D device. There is a function to extract the focus window, if it exists, but nothing for the device window. + +Heuristics +---------- + +I don't like heuristics. I'm a thoroughbred pessimist; I keep assuming the worst case. This makes me good at computer security and tricky optimizations (if I can convince myself it's safe and correct, it is), but it also means I'm skeptical to several forms of technology, including machine learning, VR, smart home, DRM, and heuristics. + +But in this case, it doesn't seem like I have any choice. + +I'll have to enumerate the program's windows, and check if there's exactly one visible. If yes, and this window has visible child windows, that's the target; if none, multiple, or no children, disable this functionality, to minimize the risk of breaking anything else. + +A few glitches later, mostly caused by forgetting that the original PresentImage does more than just submit the swap chain, and it works. + +Unfortunately, there's still trouble... in the original microkiri, clicking anywhere returns to the menu. With my custom objects, it gets stuck somewhere. + +Some of it is because I screwed up the threading. This is easy to fix with some printf debugging. + +Some of it is due to reference cycles and other refcounting accidents. I don't have good debugging tools for those, but the bad ones (shotgun debugging) worked well enough. (Some filters held references to secondary interfaces on the filter graph - filters are not allowed to reference the graph, that's a reference cycle.) + +Some of it a segfault somewhere during shutdown. Or, well, whatever this is a symptom of. + +![image](making/segfault.png) + +(It was a segfault, I didn't expect JoinFilterGraph to be called with null arguments.) + +And, of course, that wasn't the last shutdown issue... turns out the IVMRSurfaceAllocatorNotify9 interface has a separate refcount from the rest of the VMR-9. Wine even has a comment that Devil May Cry 3 demands it works that way; turns out DMC isn't the only one. I don't know if that's legal per COM rules, but those rules seem pretty poorly enforced anyways. + +Naturally, that wasn't the last shutdown issue either... while the window is getting destroyed and memory is no longer leaking, the image still isn't showing up. Is this yet another Wine bug? + +Even worse hacks +---------------- + +I don't feel like debugging that thing. Even if I can figure out what exactly it's doing, there's a good chance I can't hack over it properly. + +Instead, let's go for an improper hack; these hacks are becoming worryingly large. Let's change the child window to a borderless popup window, owned by and positioned over the real one. This will screw up if the parent window is moved while the movie is playing, or if your window manager doesn't let me position windows the way I want, but this entire project is an awful hackjob anyways. + +The first step is take one of my testbench programs and make it rearrange the windows, to see if it works. + +Unsurprisingly, it does not. Changing the child to a toplevel window works, but that gives it an entry in the taskbar, and allows it to go below its parent. + +Some additional guesswork reveals that if I add an extra ShowWindow(SW_HIDE) ShowWindow(SW_SHOW), it works. I should do that anyways, so it doesn't try to create then immediately destroy it as a proper toplevel. + +Surprisingly, it works. After the obligatory fixes to move the window later (Kirikiri also moves the window, and doesn't know it's no longer a child window) and keeping the window in the top left corner of the screen while testing (a bug I've seen before, in the Heroes of Might and Magic III map editor), of course. + +WMV +--- + +Now that the MPEG video works, it's time for next challenge - the WMV video. Connecting my debugger reveals a failed assertion, saying + +Failed to call ConnectFilters( pWMSource, pWMVDec ). : [0x80004005] Error: 0x80004005 + +First step, copy enough of Kirikiri into my testing framework that I can reproduce the failure... + +and it turns out it fails in CWMOutput::GetMediaType, becuase WMCreateSyncReader SetReadStreamSamples is incompletely implemented. It expects one of 15 named GUIDs (which is actually one of 13 because WMMEDIASUBTYPE_WMAudioV2/7/8 are identical - and I had to google up what value that GUID has, because my mingw headers don't have it), and KSDATAFORMAT_SUBTYPE_PCM is not one of those GUIDs. Which means that pin returns zero output formats, it has nothing to connect to the decoder pin, and the connection fails. + +Next step: Figure out how to hijack that... unlike the previous objects, which were created by CoCreateInstance, this one is LoadLibrary("wmvcore.dll")->GetProcAddress("WMCreateSyncReader"). It's not internally implemented via CoCreateInstance either. + +Luckily, the VMR9 is created before the WMV graph. So, how about, upon creation, that object... rewrites... krmovie's import table... and swaps out GetProcAddress... for a custom one that returns a homemade object that returns a compressed media type......... + +Does the ugliness ever end +-------------------------- + +Oh, and Wine doesn't implement CLSID_CWMVDecMediaObject and CLSID_CWMADecMediaObject either. I'll have to either implement them properly, or just send uncompressed output from the SyncReader with a fake media type, and make the DMOs just swap the media type and nothing else. That latter part doesn't require anything too undocumented, it's just CoRegisterClassObject; hopefully nothing else in the process wants the real WMV decoders. Why does Kirikiri not just send decompressed output from the SyncReader directly to the VMR9, anyways? + +So, first step is hijack krmovie's GetProcAddress... second step is hijack _rmovie.dll instead, krmovie.dll is a hacked wrapper module I made myself (the more places I have to inject debug logging and test code, the better)... yep, here's WMCreateSyncReader. And DecodePointer and EncodePointer, two of the Windows API's most useless functions (alongside SetRect and some of its friends)... sure, I'll just delegate that to the real GetProcAddress. This also gives me an opportunity to hijack Direct3DCreate9, but the only thing that'd give me is a slightly less hacky way to determine which child window is the render target; this thing works, it's good enough. + +Filling in COM objects is boring, but straightforward. Swapping out the media type is also straightforward; when Kirikiri calls IWMMediaProps::GetMediaType, ask for 16 bytes more than the real media type struct, and place the real subtype GUID there. This augmented media type will then show up in IMediaObject::SetInputType, where I can recover the real one. + +Just need to fill in IMediaObject functions in the order they're called, the rest can just return E_OUTOFMEMORY... GetStreamCount, AllocateStreamingResources (and FreeStreamingResources), SetInputType, GetOutputSizeInfo (just stub out the size for now), GetOutputType, SetOutputType, ProcessInput and ProcessOutput... need to fill in the timestamp too, but I can just copy that from ProcessInput, and, surprisingly... + +...it works. Both video and sound are correct. Didn't even need to mess with the VMR9, what I did earlier is enough. + +Segfaults on shutdown, though. And I need to dehardcode the output size. But it's a good start. + +Fill in Flush(), fill in SetInputType(DMO_SET_TYPEF_CLEAR)... ...why is it crashing with error E06D7363? The opposite would make sense, it was reading a null pointer. What is E06D7363 even? Uncaught C++ exception? Who's throwing anything? + +Is it this m_Surfaces.resize(*lpNumBuffers) call? What is *lpNumBuffers? That pointer is passed to AllocateSurfaceHelper, do I have anything injected there? + +Turns out I do. And that function is, for some reason, changing *lpNumBuffers to zero, with a return value of D3DERR_INVALIDCALL. (For MPEG videos, which work, it returns ERROR_INVALID_PARAMETER, leaving the buffer count unchanged. Zero is a fine argument to std::vector<>::resize, but it works less well for the subsequent .at(0).) + +Turns out said reason is the pixel format was D3DFMT_R8G8B8; should be D3DFMT_X8R8G8B8. And the 888 comes from MEDIASUBTYPE_RGB24. + +Changing that is easy - but it still crashes, seemingly with a use-after-free. Probably something wrong with my VMR9 memory management... it has two different refcounts. Four if counting the ones in Wine's implementation. I think I'll need to redesign that... + +...or just check how Kirikiri does it, sounds easier... looks like it doesn't keep any reference to the VMR9's IBaseFilter around, but it does keep an IVMRMixerBitmap9 around. If I implement that interface and steal its reference... + +...no difference, of course, still crashes. + +Let's see if I can convince this thing to emit a YUV format instead, I know YV12 works... the obvious one would be IWMMediaProps::SetMediaType, but that's a stub. The only function that sets output format seems to be IWMSyncReader::SetOutputProps. + +Let's just call that... ...wow, it worked. Shutdown works fine too. Then let's keep that solution. + +Which means... +-------------- + +The task is done. The videos work. Not perfectly (there's an off-by-2x2 on the window position, and the code is awful), but good enough for me. + +Next up, there's plenty of debug prints and shitty formatting to clean up, but that's so boring it's barely worth mentioning. + +Let's check my checklist from earlier... + +- \[X] Investigate WMV/WMA support, and fix if necessary +- \[½] Implement it by replacing COM objects only, no custom krmovie.dll, so it can be installed into Wine and not into individual games; ideally also detect if the Wine bugs got patched, so my hijacks can hide themselves +- \[½] Get the same behavior as Windows, without a separate window +- \[X] Investigate shutdown behavior, I think there's some memory leaks and/or use-after-free +- \[ ] Get it into Proton +- \[ ] Investigate other VN engines; Kirikiri is the most common, but not the only one (though I suspect most of them suffer from the same bugs. Maybe they even copied code from each other.) +- \[ ] Enjoy my hard-earned VNs + +Some of them are incompletely solved, but they're good enough. + +Now let's try some random proper VN (for example Wagamama High Spec Trial Edition) and see what happens... + +...OP movie works. It doesn't even show that off-by-2x2. It does glitch out, as expected, if I move the window, but I wasn't planning on doing that anyways; I prefer to play my VNs in fullscreen. (Testing them doesn't count as playing.) + +The logo, however, does not, it's just black. And there are occasional Error Aborts if I skip the movie... whatever, I'll just not do that. + +The logo is using my MPEG splitter, but there's no VMR9. Probably using one of krmovie's other three main entry points - microkiri uses only GetMixingVideoOverlayObject, but krmovie also exports GetVideoLayerObject and GetVideoOverlayObject. + +The easiest way to track that down is to ask the MPEG decoder's proposed output pin what it is. It nicely responds that it's a Buffer Renderer, which is a Kirikiri thing that accepts only MEDIASUBTYPE_RGB32 and MEDIASUBTYPE_ARGB32 input; my MPEG decoder only implements MEDIASUBTYPE_YV12 output. + +Well, that's fixable. + +But, of course, not enough, it won't play the video. I don't feel like playing 20 Questions with what's breaking, so let's install my fake krmovie.dll into Wagamama and see where it fails. + +Failed to query IAMStreamSelect. : [0x80004002] Error: 0x80004002. That's clear enough. Apparently the MPEG splitter needs that interface if the file has an audio channel. I thought this one didn't, but apparently it does, it's just empty. Good find, better find it now than after I think development is done. + +Sure, let's fill that in. (Fun fact: Wine's CLSID_MPEG1Splitter doesn't implement it either.) + +Segfault. Of course. Should've expected that. + +At least this one is easy to track down. It's because of a VFW_E_NOT_COMMITTED in my IMemAllocator, which in turn is because committing returns VFW_E_SIZENOTSET, because I'm asking for a size of zero, because I'm using the bits per pixel value before setting it (it's only set when connecting the output pin, but the allocator is initialized when I connect the input). + +Sure, let's initialize the allocator when connecting the output instead. And ... yep, here's an image. Just need to convert YV12 to xrgb8888. pl_mpeg has a function for that, just need to hook it in. + +![image](making/closeenough.png) + +Close enough (I guessed wrong on plm_frame_to_rgba/bgra/argb/bgra). And apparently Kirikiri wants it upsidedown; sure, that's also easy to fix, plm_frame_to_x's stride argument is int, it can be negative. Many Windows functions accept right-side-up images by setting height negative, but Kirikiri doesn't seem to handle that properly, so upsidedown it is. + +Well, that's the last issue on the Windows side, unless krmovie's GetVideoOverlayObject function does something weird. Now all that's left is the installer, the GStreamer-side codecs (spending all that time just to end up with protonmediaconverter is not an improvement), and releasing the thing. + +Or if a Layer video is WMV; my hijack is installed when a VMR9 is created, but only Overlay videos use VMR9. + +The only COM object that every code path creates is CLSID_FilterGraph, so I guess the best (least bad) solution is to hijack that... it's annoying, but straightforward. And since it's the first one created, I can make creating it register all the other objects, simplifying installation. + +Proton +------ + +First (why am I using that word after 5000 words?), let's throw an mpeg into one of my test tools, then throw that into standard Proton and see what happens. + +It fails to create CLSID_CMpegVideoCodec, then creates a CLSID_decodebin_parser that ends up with protonmediaconverter. As expected. + +Now let's install krkrwine. + +Actually, before that, let's create an installer. How does Proton look... + +...like a giant mess, what a shocker... well, the task it's performing is also a giant mess, so it makes sense. + +Based on Proton's architecture and my goals, I believe the best solution is copying the files to the main Proton installation, and adding a small snippet to user_settings.py that reinstalls krkrwine into this Proton prefix. + +... Okay, installation works, and krkrwine's MPEG decoder works. Even the sound works, via Wine's builtin one, using mpg123audiodec. Now let's ask for decodebin... ...why is it getting stuck? + +Why is it getting stuck if I run these GStreamer elements in Wine, but not in gst-launch? + +Why does it run through the entire video before connecting? + +Does it need the duration or something? + +Seems that it does. Let's just hardcode five seconds because I don't care... well, that plays something. Prints a billion "gst_segment_to_running_time: assertion 'segment->format == format' failed", though. And cuts off the first second of audio. + +Maybe I should try again with installing the Glorious-Eggroll codecs... they didn't help when the codecs weren't implemented on the Windows side, but that problem doesn't exist anymore. + +Most of the GStreamer filters I need are in libgstlibav.so, which is present in Proton. But I guess ffmpeg isn't, so let's check what it tries to load that isn't present... that is libswresample.so.3, libavutil.so.56, libavformat.so.58, libavfilter.so.7 and libavcodec.so.58. Let's copy them and see what happens... + +...nope, just protonmediaconverter. GST_DEBUG log says... Added plugin 'libav' plugin with 14 features from binary registry. That's probably wrong, it should've gained a few more... but probably not while it's cached in that registry thing. Let's just delete it. And then delete the correct registry... nope, still mediaconverter. Looks like it loads those libraries from some base Steam runtime. Then avdec_mpeg2video must be implemented somewhere else... what are those 14 features, anyways? Can I decode the registry? + +GStreamer +--------- + +The registry format is undocumented other than GStreamer's source code, but that's good enough. The 14 features are avdeinterlace, avmux_flv, avdemux_gif, avdec_valve_h264, avdec_mjpeg, avdec_h264, avdec_gif, avdec_apng, avdec_pcm_vidc, avdec_pcm_lxf, avdec_mp3, avdec_flac, avdec_alac, and avdec_aac. I don't know why there are two h264 decoders, but too many isn't a problem; too few, like avdec_mpeg2video, is a problem. + +Actually, I doubt that's the only missing one... I need the filters mpegpsdemux (from libgstmpegpsdemux.so), mpegvideoparse (libgstvideoparsersbad.so), avdec_mpeg2video (libgstlibav.so), mpegaudioparse (libgstaudioparsers.so), and avdec_mp2float (libgstlibav.so again). + +mpegaudioparse exists, so I can ignore that. Let's try something easier - let's copy libgstvideoparsersbad.so and libgstmpegpsdemux.so, and see if they're compatible with this Proton... preferably to dist/lib64/gstreamer-1.0/, not dist/lib64/... yep, that got mpegpsdemux working. + +Though mpegvideoparse complains about Failed to load plugin '/home/x1/steam/steamapps/common/Proton 8.0/dist/lib64/gstreamer-1.0/libgstvideoparsersbad.so': libgstcodecparsers-1.0.so.0: cannot open shared object file: No such file or directory. Sure, I can copy that file too. + +Well, that's three of five necessary elements working. Next up, I need to figure out why libgstlibav won't register mpeg2video and mp2float... probably because av_codec_iterate() doesn't return them for some reason... can I inject a custom Linux program that calls that function? + +Turns out I can, by adding os.system("LD_DEBUG=all LD_LIBRARY_PATH=/home/x1/steam/ubuntu12_64/video/ /home/x1/a.out") to user_settings.py. It says the list of codecs is h264_vaapi, apng, gif, h264, mjpeg, valve_h264, aac, alac, flac, mp3, vorbis, pcm_alaw, pcm_bluray, pcm_dvd, pcm_f16le, pcm_f24le, pcm_f32be, pcm_f32le, pcm_f64be, pcm_f64le, pcm_lxf, pcm_mulaw, pcm_s8, pcm_s8_planar, pcm_s16be, pcm_s16be_planar, pcm_s16le, pcm_s16le_planar, pcm_s24be, pcm_s24daud, pcm_s24le, pcm_s24le_planar, pcm_s32be, pcm_s32le, pcm_s32le_planar, pcm_s64be, pcm_s64le, pcm_u8, pcm_u16be, pcm_u16le, pcm_u24be, pcm_u24le, pcm_u32be, pcm_u32le, pcm_vidc, libvpx, libvpx-vp9; this is ... slightly fewer than it should be. + +There's also a friendly avcodec_configuration() function, that returns, among others, --disable-all --enable-parser=h264 --enable-decoder='aac,h264,gif,vorbis,mp3,flac,alac,pcm*,valve*,apng,libvpx_vp8,libvpx_vp9' --enable-demuxer='aac,matroska,gif,ogg,mov,mp3,flac,wav,flv,apng' --disable-decoder='h263,hevc,mpeg1video,mpeg2video,vc1,vp8,vp9' + +which is slightly less than I require. + +Glorious Eggroll +---------------- + +Which means I need to copy in a better ffmpeg, probably from Glorious Eggroll. + +And I need to find somewhere to install it to. + +If I'm reading Proton's source code correctly, the first two entries of LD_LIBRARY_PATH - the two that are searched first - are ~/steam/ubuntu12_64/video and ~/steam/ubuntu12_32/video. Number three and four are ~/steam/steamapps/common/Proton 8.0/dist/lib64 and ~/steam/steamapps/common/Proton 8.0/dist/lib. + +I don't want to overwrite any files, so I can't swap out the ones in ubuntu12_64. However, I can remove those directories from the search path from within user_settings.py; if I do that, I can copy an ffmpeg from Glorious Eggroll to dist/lib64, and it should work. + +Right? + +Of course not. Glorious Eggroll's ffmpeg doesn't have MPEG-1 either. + +Though it does have a few more features; libgstlibav now offers the 19 features avdeinterlace, avdec_wmv3image, avdec_wmv3, avdec_wmv2, avdec_wmv1, avdec_vc1, avdec_msmpeg4, avdec_msmpeg4v2, avdec_msmpeg4v1, avdec_mpeg4, avdec_h264, avdec_h263, avdec_xma2, avdec_xma1, avdec_wmav2, avdec_wmav1, avdec_wmapro, avdec_wmalossless, avdec_aac. + +Almost all of them are new, only avdec_h264 and avdec_aac remain. Even the mysterious avdec_valve_h264 is gone. But of the removed ones, only MP3 seems useful in Proton/Wine, and there's another MP3 decoder (mpg123audiodec). In a generic media player, removing FLAC and ALAC would be a loss, but if Glorious Eggroll excludes them, then Windows most likely excludes them too, so nobody's using them. + +GStreamer again +--------------- + +Well, that solves some problems. I was hoping I could ditch my PL_MPEG module, but it doesn't seem so. At least I can get rid of the demuxer, I'm sure mpegpsdemux does a better job than me at estimating video length. + +...oh, and I just realized that if the WMV decoder is connected to a Kirikiri Layer video object, the decoder will offer YV12, but Layer won't know what to do with that. I'll have to shuffle around the Wine-side format negotiation a bit. Let's put that on the todo list and forget it for now... + +so I need to create a fake element that just creates another element, and forwards everything to there. How hard can it be? + +Spoiler alert: Very. GStreamer does a lot of weird things I don't understand. + +In fact, it does so many weird things I'm just going to give up. I'll leave the hard parts to GStreamer; I'll just create a decodebin and connect to its autoplug-sort signal. + +Some annoying work later (seemingly partially caused by creating the pads too late), that works too. + +That's one entry less on the todo list... all that's left is that WMV/Layer todo, and double checking everything in Proton. I suspect both will be easy. Just like the last ten times I thought it'd be easy. + +Checking in Proton reveals ... nothing noteworthy whatsoever. Everything works as expected. It's nice to be wrong about being wrong sometimes. + +Now for the WMV/Layer thing, should also be an easy change... ...nope, of course not. + +One grueling debug session later, with MEDIASUBTYPE_YV12-formatted textures that are treated as RGB, reveal that Wine is checking the biCompression field, which I forgot updating. Half a minute to fix, three hours to discover, as usual... oh, and the crashes on shutdown are back. + +Those crashes are easy to fix if I can get a stack trace. Wine has a builtin crash handler that prints stack traces (and a bunch of assorted garbage) - but Kirikiri has its own crash handler, which often takes precedence. And gdb is flaky in Wine. + +But I guess gdb is worth a try. + +It worked. Gave me a clean stack trace saying that the IMediaObject::SetOutputType format argument is null. DirectShow sure likes passing nulls to disconnect stuff... whatever, don't care, easiest solution is just returning E_POINTER aka 'uh dude why is that argument null? I don't want nulls'. It's not like Wine can do anything with that error. + +And now that problem is gone. + +Now to check Wagamama in fullscreen... flawless. (Well, it does stupid things with clamping the mouse position, but that's not video related. Worst case, I'll switch to windowed mode.) + +Done +---- + +Only a few tiny things, and one less tiny, left to do. + +- Check if I can remove libgstvideoparsersbad.so; I copied it when I thought I could acquire avdec_mpeg2video from Glorious Eggroll, but PL_MPEG has the mpegvideoparse element built in, so it shouldn't be necessary anymore. +- Check if I have any inappropriate debug code in user_settings.py that interferes with anything. +- Update the krkrwine installer to copy the GE ffmpeg and stuff. +- Test it in proper Proton. +- Release this crazy thing. +- Enjoy my hard-earned VNs. + +Naturally, the final testing steps required for that give me a handful of trouble as well... partially due to a leftover remote/data_emergency.ksd save file created when this one VN crashed when I actually read it. And, of course, the mandatory portion of not working. + +I think I'll have to go back a few steps and run microkiri in Proton... which, of course, is blocked by a pressure-vessel-locale-gen: Unable to find ja_JP in /etc/locale.gen /usr/share/i18n/SUPPORTED Assuming UTF-8 and hoping that works... + +Can be fixed with a quick `import __main__; __main__.g_session.env["LC_ALL"] = "ja_JP"` in user_settings.py, but... sigh. So many roadblocks. + +Doesn't work here either, of course. + +Let's go back another step, to the standalone video player thing... still breaks. Is mpg123audiodec doing something different from avdec_mpeg2float? + +And the WMV is broken too; video works, but audio is not. Seemingly because it chose protonaudioconverterbin. Seemingly from inside my krkr_fakewma element. Let's debug that first... ...well, that was easy. Forgot an exclamation mark, so it put the protonaudioconverters first instead of last. + +Next up, try to get non-Proton Wine to choose mpg123audiodec... preferably without breaking the existence of avdec_mpeg2float... hm... do I, by any chance, have any GStreamer elements nearby whose only job is to override the rank when decodebin tries to pick the best element? + +Not done +-------- + +So there's something wrong, either with mpg123audiodec, or with the way I use it... and it does indeed not work under Wine. But because I'm such a good customer, I've been rewarded with another bug as well. + +I suspect it isn't a problem with mpg123, but with my wrapper. Maybe I'm creating the output pad too late. + +Back to the drawing table, let's find another design... maybe create four different elements depending on WMA version so I can immediately create the correct element. In C, without C++ templates. (Or figure out how to get C++ running in 32bit.) + +For now, let's just hardcode audio/mpeg and mpg123audiodec so I can reproduce the same issue I saw in Proton... yep, that works. The problem is... the audio thread gets stuck... in IMemAllocator::GetBuffer... let's just increase buffer count from 1 to 16 and see what happens... yep, that worked. + +Next up, let's deal with that WMA thing... how do I best emulate templates in C... how do these G_DECLARE_FINAL_TYPE macros work, anyways? Maybe I should work with the GObject type system instead, and create the classes at runtime. + +...looks easy enough to dynamically create a bunch of classes. Now how do I get the pad template into the class... there is a class constructor, but it only takes the uninitialized class descriptor as its single argument. I could use a global variable, but mutable global variables are generally not recommended. + +Instead, let's fill it in after the class is registered. As long as GStreamer doesn't create any instance before we're ready, it's safe enough. + +Now how do I get the GTypeClass* from a GType, as in the opposite of G_TYPE_FROM_CLASS... ...what, there is no such function? That can't be right, let's check gtype.c... I know that there's an allocation of class_size bytes somewhere, let's find it... why is everything named weird stuff like type_class_init_Wm... the allocation is stored in ... node->data->class.class? That looks annoying to search for, let's limit it to class; and hope something returns it... looks like g_type_class_peek is my best bet? It returns void\*, which can then be casted... + +Segfault. + +That function returns null. + +Because that class isn't actually initialized when it's registered. There's a g_type_class_ref function nearby, let's try that. (Let's not bother deleting the reference; it's registered as a static class, so it's permanent.) + +Success. + +Next problem, how do I find that class descriptor from within the class initializer? I can't find any way to find a GObject's true GType, only to ask if it's a specific class or a child thereof. + +I guess I'll have to do that... store the GTypes in some global variables, loop em until finding the child's type, and grab the pad template from there. + +Segfault. + +The child is of none of the types. + +Because that's the parent object's constructor. It's not the child class' type yet. Let's just move that... done, that works. + +...actually, how do GStreamer's ffmpeg wrappers work? They must've had the same problem as me. + +Turns out they ... use g_type_set_qdata to get the information into the class initializer, then G_OBJECT_GET_CLASS to get the class descriptor. Those macros are so hard to find... but it sounds cleaner, so let's rewrite some more code... luckily, all I need to get into the class initializer is two strings (element name and caps), so if I concatenate them with a \0 separator, I can keep things simple and get it into a single qdata, allowing me to define each one with a function call, not a struct. Feels cleaner than looping. + +WARNING: erroneous pipeline: no element "krkr_fakewmav2" + +That's not in the blueprints. Let's add more logging in the constructors... why is only krkr_fakewmav1 being registered? + +Is it because I typoed some && as ;, so the subsequent registrations became dead code? + +Yes, it was. Feels like the kind of thing GCC should warn about... + +Well, that's two bugs less, let's try it in Proton again... those missing warnings sound like compiler bugs, but there's no way they're not known already, so no point filing duplicate reports. + +Proton again +------------ + +MPEG with Wine-side decoding... works fine, including sound. WMV with GStreamer-side decoding... good too. MPEG with GStreamer-side decoding... + +0:00:00.277951788 4445 0xe2a1a718 WARN basetransform gstbasetransform.c:1370:gst_base_transform_setcaps: transform could not transform video/x-raw, format=(string)YV12, width=(int)0, height=(int)0, framerate=(fraction)14131411/42292998 in anything we support + +I think I'll count that as not fine. Apparently that mpegvideoparse element wasn't fully optional after all. Easy fix, just yet another roadblock on the way to VNs... + +Now let's try microkiri again... nope, both give black screens. WMV repeats the first second of sound; MPEG isn't supposed to have any sound. Debug logs reveal that MPEG is sending the frames somewhere, but they're not showing up. Logs also mention DXVK, as opposed to wined3d; maybe it doesn't need that window moving hack. Let's just turn it off... wow, that worked. It made an image show up on the WMV as well, but it still gets stuck somewhere. + +Next up: How to identify wined3d (where it's needed) vs DXVK (where it just breaks things)? + +IDirect3D9::GetAdapterIdentifier(), maybe? ...no, I don't think igdumdim32.dll or Intel(R) HD Graphics 4000 are the strings I need. + +Check Wine's source for anything I can QueryInterface, maybe... nope, nothing. Check DXVK... that one offers a handful of strange objects, like ID3D9VkInteropDevice. I'll just use that, and hope no third Direct3D9 implementation (like whatever Windows does) shows up. + +Now for the WMV... it's clearly almost working, let's just print something whenever these DMOs process anything... why is it calling ProcessOutput in an infinite loop? + +Turns out the DMO_OUTPUT_DATA_BUFFERF_INCOMPLETE flag was set, for some reason. Shouldn't be. Well, easy to remove. And as an extra safety, it should return S_FALSE if it did nothing. + +Well, that's yet another couple of bugs deleted, let's try a real VN... + +...the pre-titlescreen logo works, but the OP movie does not. Unsatisfactory. Let's try that demo in normal Wine... + +...logo is black, OP movie throws a message box and terminates the process. Let's add some debug logging... + +...now it works. What. How is that even possible? And recompiling switches whether it works, completely at random. Let's set WINEDEBUG=+loaddll... + +...how can it not even load krmovie.dll? + +Why does ls -l mark the DLL red? + +Oh right, I rewrote that file to a symlink to my hijacked krmovie, and it works or not depending on whether it exists, i.e. whether I did make all since the last make clean. Let's put a dependency in the Makefile... + +and now everything works. And I found a bug in krkrwine.dll - it never unloads. krmovie.dll, however, can unload; if that happens, the new one will not have the hijacked GetProcAddress that improves WMCreateSyncReader. + +It even explains why it doesn't break in Wine - because my custom krmovie.dll never unloads the real one. My wrapper gets unloaded and reloaded, but that does nothing. + +The easiest workaround is simply LoadLibrary krmovie.dll instead of GetProcAddress, so it too remains loaded. + +Now, let's see if that was enough... will this VN finally run correctly? + +It does +------- + +not. Partially due to compiler optimizations in my fake krmovie (or rather due to me doing something stupid which the optimizations hide), partially due to e06d7363 C++ exception. Is it doing something stupid with pixel formats again? It's creating a bunch of OpenGL elements there, I don't recall seeing that in Wine. + +Adding debug logging reveals that... it's not setting the output format at all, it's just asking which it supports. + +Last time I saw C++ exceptions, they were from a std::vector after IVMRSurfaceAllocatorNotify9::AllocateSurfaceHelper zeroed an array size, so let's log that. ...nope, nothing. + +Checking the Kirikiri source code reveals ...that suspicious-looking ThrowDShowException function again. I'm so used to having my fake krmovie installed that I forgot that it does, in fact, throw in the standard game. I guess the easiest solution is reinstalling it. + +...nope, that just tells me + +致命的なエラーが発生しました。 +ファイル : custom.ks 行 : 93 +タグ : 不明 ( ← エラーの発生した前後のタグを示している場合もあります ) +Cannot convert the variable type ((void) to Object) + +which, while good to know, isn't quite the result I was hoping for. + +Some quick (or not) staring at source code reveals that I was looking for _rmovie.dll (the real krmovie.dll) in wrong place. One quick fix later, and I'm greeted with + +TVPThrowExceptionMessage: Failed to call ConnectFilters( pWMVDec, pRdr ). : [0x80004005] Error: 0x80004005 + +(and some Proton minidump handler getting into some absurd loop and catching the same problem 9999 times until the process terminates. I hope it's not uploading those dumps anywhere...) + +Additional log reading reveals Kirikiri is calling GetVideoLayerObject for both videos, which, as I discovered a while ago, demands RGB32 or ARGB32. Sure, let's implement that... hopefully the WMV decoder is able to output that format, but I saw a list of supported pixel formats, which includes YV12 and does not include RGB32. Nor RGB24, so maybe it's possible to switch from it but not back to it? + +I'll try to switch to RGB32 anyways. If it fails, I'll just write my own RGB24 to RGB32 unpacker, it's an easy format. + +...which raises the obvious follow up question of why this game acts differently from its own demo. But that is not a question I'll ever get answered... all I can do is implement the features one by one until everything works. + +Upgrading from two supported pixel formats to three is a lot easier than 1->2. And the WMV decoder is fine with this output format too. I guess that format list is used only if someone asks for a list of supported output formats, and the actual format list is longer than that. Yet another question I'll never get answered... + +But it still fails to connect. And that is a question I need answered. + +Let's see if Proton has the TRACE debugging compiled in... it does. Why is it returning VFW_E_BADALIGN? Why is Kirikiri demanding that alignment is 1? RGB32 data should have alignment of 4, and too high alignment has never broken anything. + +Whatever, I can do that... yep, this gets further. Still doesn't work though, it says TVPThrowExceptionMessage: Error Abort. : [0x80040211] Error: 0x80040211. And before that, 3166.707:0128:02dc:err:quartz:StdMediaSample2_SetActualDataLength Length 3686400 exceeds maximum 2764800. Who's calling SetActualDataLength? It doesn't seem to be me. + +Seems to be something in wmvcore. Seems that it doesn't reconfigure the allocator if configured to a pixel format with greater bits per pixel than the default. Seems it doesn't support RGB32 after all. Yet another Wine bug... homemade rgb24->rgb32 converter it is, then. But first, let's just give it rgb24 and say it's rgb32, so I can see if there's anything else wrong... + +...of course there is. Someone is calling BaseMemAllocator_Decommit on the audio allocator. Seems to be a threading issue. Who's using threads incorrectly, and how will I have to work around that? + +It's IWMSyncReader::GetNextSample() returning VFW_E_NOT_COMMITTED... is any other thread inside the WMReader at that point? In SetRange(), maybe? Let's log that... + +...what. It works (except the pixel format mismatch). I hate threading issues, they keep hiding as soon as I try anything. And checking Wine's WMReader source code reveals that there's a CRITICAL_SECTION around everything. + +Which means the allocator is being accessed from the outside. Which means the allocator is accessible from the outside. + +And Kirikiri does indeed seem to pass in its own allocator for some reason. + +Unfortunately, a few nearby pieces are from the DirectShow base classes, whose source code isn't available here, making it hard to determine what exactly it's supposed to do. + +What I can do is simply retry that function call if it returns that specific error. ...nope, now it just gets stuck. The log contains ... not an infinite loop of debug prints, but a stack overflow. Mostly because I forgot a parent-> when removing some debug prints... ah well, easy to fix... like the other 99999 bugs that were easy to fix... + +...now it deadlocks on shutdown, probably due to an infinite loop of VFW_E_NOT_COMMITTED. Let's add a max retry count, and a delay... ...nope, that fires too quickly. I guess I'll have to track it down properly... + +- The guilty allocator is created in BaseOutputPinImpl_DecideAllocator +- The allocator is committed inside dmo_wrapper_init_stream(), called by filter_Pause(), called by the graph builder's IMediaControl::Pause() +- It seems to be decommitted and recommitted somewhere while flushing, probably due to GST_Seeking_SetPositions() +- The flush cannot complete while GetNextSample() is running + +so maybe I should force that thing to return instead, by changing VFW_E_NOT_COMMITTED to S_FALSE. + +Segfault. Of course. + +Maybe I should google those DirectShow base classes... yep, that worked. I'm not good at doing things in right order... + +can't find anything useful there, though. No symbols in the stack trace either, but maybe I can disassemble it... + +Here's an integer constant equal to NS_E_NO_MORE_SAMPLES... yep, that's some relevant code. And an AddRef immediately followed by Release of the same object... sure, why not. Either way, apparently that function shouldn't return S_FALSE; it should be NS_E_NO_MORE_SAMPLES. I got my GetNextSample()s mixed up. + +That works. I don't like that solution, but I don't like any other part of this program either, so good enough. + +Now one final round of testing... need to both interrupt OP movie, and watch it to the end... + +It works +-------- + +... + +...surprisingly, it actually does. I think there are some audio/video sync issues, but I don't care... this is good enough. + +So that's finally, FINALLY, the last of these stupid issues. + +All that's left is packaging it up, putting it on Git, and letting people laugh at the obnoxiously winded path I took through this program. + + + +In conclusion, I've encountered, and had to fill in or work around most of, the following Wine bugs and omissions while creating this tool: + +- CLSID_CMpegVideoCodec is unimplemented +- CLSID_MPEG1Splitter only supports audio output, and doesn't implement IAMStreamSelect properly +- CLSID_VideoMixingRenderer9 ChangeD3DDevice is incomplete +- CLSID_VideoMixingRenderer9 NotifyEvent is a stub +- WMCreateSyncReader SetReadStreamSamples is incomplete (and not even tagged semi-stub) +- CLSID_CWMVDecMediaObject and CLSID_CWMADecMediaObject are unimplemented +- IVMRSurfaceAllocatorNotify9 AllocateSurfaceHelper can return D3DERR_INVALIDCALL and zero out *lpNumBuffers if originally 1 (I don't know if real Windows lets the corresponding call succeed, or if it fails and leaves *lpNumBuffers unchanged, but Kirikiri cannot handle any zeroing) +- Direct3D 9 under wined3d doesn't work in child windows +- Direct3D 9 under DXVK doesn't work in former child windows promoted to borderless standalone (I didn't investigate this one very closely) +- SetWindowLongPtr(GWLP_HWNDPARENT) does not properly create an ownership relation unless the window is hidden and re-shown (unconfirmed what that does on Windows) +- At least one drawing API (probably the HDC family, or some subset thereof) only works if the window is in the top left part of the screen +- Clearing WS_CLIPCHILDREN, drawing on the parent with Direct3D 9, then restoring WS_CLIPCHILDREN, seems to break drawing on the child window using the above drawing API (I didn't investigate this one very closely) +- WMCreateSyncReader()->IWMSyncReader::SetOutputProps() doesn't reconfigure its allocator; this causes various trouble if the requested pixel format (for example RGB32) has more bits per pixel than the default (RGB24) +- Something is causing IWMSyncReader::GetNextSample() to occasionally return VFW_E_NOT_COMMITTED. Unfortunately, I haven't been able to narrow this one down more than 'turn off the VFW_E_NOT_COMMITTED check, and watch the Wagamama High Spec OP video in Proton'. +- sink_NewSegment() calls TRACE("pin %p %s:%s, start %s, stop %s, rate %.16e.\n", ...). It renders as [...] start 0.0, stop 95.6, rate e. diff --git a/making/closeenough.png b/making/closeenough.png new file mode 100644 index 0000000000000000000000000000000000000000..94353a9c30f9390df65cea1c0141654975ac315a GIT binary patch literal 9677 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

a|Gl0I7y_%p@sW_1rb=qp7+QY25D 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 z1=s}u0000000000000000000000000000000000hfNp?b3+bKc00000NkvXXu0mjf DLFy~8 literal 0 HcmV?d00001 diff --git a/making/segfault.png b/making/segfault.png new file mode 100644 index 0000000000000000000000000000000000000000..ac4700314d5bff66109275edf47f744136e895b0 GIT binary patch literal 12278 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 NprW85U-ldd|1Sjjy.data, frame->cr.data, frame->cb.data +} + +// This function gets called for each decoded audio frame +void my_audio_callback(plm_t *plm, plm_samples_t *frame, void *user) { + // Do something with samples->interleaved +} + +// Load a .mpg (MPEG Program Stream) file +plm_t *plm = plm_create_with_filename("some-file.mpg"); + +// Install the video & audio decode callbacks +plm_set_video_decode_callback(plm, my_video_callback, my_data); +plm_set_audio_decode_callback(plm, my_audio_callback, my_data); + + +// Decode +do { + plm_decode(plm, time_since_last_call); +} while (!plm_has_ended(plm)); + +// All done +plm_destroy(plm); + + + +-- Documentation + +This library provides several interfaces to load, demux and decode MPEG video +and audio data. A high-level API combines the demuxer, video & audio decoders +in an easy to use wrapper. + +Lower-level APIs for accessing the demuxer, video decoder and audio decoder, +as well as providing different data sources are also available. + +Interfaces are written in an object oriented style, meaning you create object +instances via various different constructor functions (plm_*create()), +do some work on them and later dispose them via plm_*destroy(). + +plm_* ......... the high-level interface, combining demuxer and decoders +plm_buffer_* .. the data source used by all interfaces +plm_demux_* ... the MPEG-PS demuxer +plm_video_* ... the MPEG1 Video ("mpeg1") decoder +plm_audio_* ... the MPEG1 Audio Layer II ("mp2") decoder + + +With the high-level interface you have two options to decode video & audio: + + 1. Use plm_decode() and just hand over the delta time since the last call. + It will decode everything needed and call your callbacks (specified through + plm_set_{video|audio}_decode_callback()) any number of times. + + 2. Use plm_decode_video() and plm_decode_audio() to decode exactly one + frame of video or audio data at a time. How you handle the synchronization + of both streams is up to you. + +If you only want to decode video *or* audio through these functions, you should +disable the other stream (plm_set_{video|audio}_enabled(FALSE)) + +Video data is decoded into a struct with all 3 planes (Y, Cr, Cb) stored in +separate buffers. You can either convert this to RGB on the CPU (slow) via the +plm_frame_to_rgb() function or do it on the GPU with the following matrix: + +mat4 bt601 = mat4( + 1.16438, 0.00000, 1.59603, -0.87079, + 1.16438, -0.39176, -0.81297, 0.52959, + 1.16438, 2.01723, 0.00000, -1.08139, + 0, 0, 0, 1 +); +gl_FragColor = vec4(y, cb, cr, 1.0) * bt601; + +Audio data is decoded into a struct with either one single float array with the +samples for the left and right channel interleaved, or if the +PLM_AUDIO_SEPARATE_CHANNELS is defined *before* including this library, into +two separate float arrays - one for each channel. + + +Data can be supplied to the high level interface, the demuxer and the decoders +in three different ways: + + 1. Using plm_create_from_filename() or with a file handle with + plm_create_from_file(). + + 2. Using plm_create_with_memory() and supplying a pointer to memory that + contains the whole file. + + 3. Using plm_create_with_buffer(), supplying your own plm_buffer_t instance and + periodically writing to this buffer. + +When using your own plm_buffer_t instance, you can fill this buffer using +plm_buffer_write(). You can either monitor plm_buffer_get_remaining() and push +data when appropriate, or install a callback on the buffer with +plm_buffer_set_load_callback() that gets called whenever the buffer needs more +data. + +A buffer created with plm_buffer_create_with_capacity() is treated as a ring +buffer, meaning that data that has already been read, will be discarded. In +contrast, a buffer created with plm_buffer_create_for_appending() will keep all +data written to it in memory. This enables seeking in the already loaded data. + + +There should be no need to use the lower level plm_demux_*, plm_video_* and +plm_audio_* functions, if all you want to do is read/decode an MPEG-PS file. +However, if you get raw mpeg1video data or raw mp2 audio data from a different +source, these functions can be used to decode the raw data directly. Similarly, +if you only want to analyze an MPEG-PS file or extract raw video or audio +packets from it, you can use the plm_demux_* functions. + + +This library uses malloc(), realloc() and free() to manage memory. Typically +all allocation happens up-front when creating the interface. However, the +default buffer size may be too small for certain inputs. In these cases plmpeg +will realloc() the buffer with a larger size whenever needed. You can configure +the default buffer size by defining PLM_BUFFER_DEFAULT_SIZE *before* +including this library. + + +See below for detailed the API documentation. + +*/ + + +#ifndef PL_MPEG_H +#define PL_MPEG_H + +#include +#include + + +#ifdef __cplusplus +extern "C" { +#endif + +// ----------------------------------------------------------------------------- +// Public Data Types + + +// Object types for the various interfaces + +typedef struct plm_t plm_t; +typedef struct plm_buffer_t plm_buffer_t; +typedef struct plm_demux_t plm_demux_t; +typedef struct plm_video_t plm_video_t; +typedef struct plm_audio_t plm_audio_t; + + +// Demuxed MPEG PS packet +// The type maps directly to the various MPEG-PES start codes. PTS is the +// presentation time stamp of the packet in seconds. Note that not all packets +// have a PTS value, indicated by PLM_PACKET_INVALID_TS. + +#define PLM_PACKET_INVALID_TS -1 + +typedef struct { + int type; + double pts; + size_t length; + uint8_t *data; +} plm_packet_t; + + +// Decoded Video Plane +// The byte length of the data is width * height. Note that different planes +// have different sizes: the Luma plane (Y) is double the size of each of +// the two Chroma planes (Cr, Cb) - i.e. 4 times the byte length. +// Also note that the size of the plane does *not* denote the size of the +// displayed frame. The sizes of planes are always rounded up to the nearest +// macroblock (16px). + +typedef struct { + unsigned int width; + unsigned int height; + uint8_t *data; +} plm_plane_t; + + +// Decoded Video Frame +// width and height denote the desired display size of the frame. This may be +// different from the internal size of the 3 planes. + +typedef struct { + double time; + unsigned int width; + unsigned int height; + plm_plane_t y; + plm_plane_t cr; + plm_plane_t cb; +} plm_frame_t; + + +// Callback function type for decoded video frames used by the high-level +// plm_* interface + +typedef void(*plm_video_decode_callback) + (plm_t *self, plm_frame_t *frame, void *user); + + +// Decoded Audio Samples +// Samples are stored as normalized (-1, 1) float either interleaved, or if +// PLM_AUDIO_SEPARATE_CHANNELS is defined, in two separate arrays. +// The `count` is always PLM_AUDIO_SAMPLES_PER_FRAME and just there for +// convenience. + +#define PLM_AUDIO_SAMPLES_PER_FRAME 1152 + +typedef struct { + double time; + unsigned int count; + #ifdef PLM_AUDIO_SEPARATE_CHANNELS + float left[PLM_AUDIO_SAMPLES_PER_FRAME]; + float right[PLM_AUDIO_SAMPLES_PER_FRAME]; + #else + float interleaved[PLM_AUDIO_SAMPLES_PER_FRAME * 2]; + #endif +} plm_samples_t; + + +// Callback function type for decoded audio samples used by the high-level +// plm_* interface + +typedef void(*plm_audio_decode_callback) + (plm_t *self, plm_samples_t *samples, void *user); + + +// Callback function for plm_buffer when it needs more data + +typedef void(*plm_buffer_load_callback)(plm_buffer_t *self, void *user); + + + +// ----------------------------------------------------------------------------- +// plm_* public API +// High-Level API for loading/demuxing/decoding MPEG-PS data + + +// Create a plmpeg instance with a filename. Returns NULL if the file could not +// be opened. + +plm_t *plm_create_with_filename(const char *filename); + + +// Create a plmpeg instance with a file handle. Pass TRUE to close_when_done to +// let plmpeg call fclose() on the handle when plm_destroy() is called. + +plm_t *plm_create_with_file(FILE *fh, int close_when_done); + + +// Create a plmpeg instance with a pointer to memory as source. This assumes the +// whole file is in memory. The memory is not copied. Pass TRUE to +// free_when_done to let plmpeg call free() on the pointer when plm_destroy() +// is called. + +plm_t *plm_create_with_memory(uint8_t *bytes, size_t length, int free_when_done); + + +// Create a plmpeg instance with a plm_buffer as source. Pass TRUE to +// destroy_when_done to let plmpeg call plm_buffer_destroy() on the buffer when +// plm_destroy() is called. + +plm_t *plm_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done); + + +// Destroy a plmpeg instance and free all data. + +void plm_destroy(plm_t *self); + + +// Get whether we have headers on all available streams and we can accurately +// report the number of video/audio streams, video dimensions, framerate and +// audio samplerate. +// This returns FALSE if the file is not an MPEG-PS file or - when not using a +// file as source - when not enough data is available yet. + +int plm_has_headers(plm_t *self); + + +// Get or set whether video decoding is enabled. Default TRUE. + +int plm_get_video_enabled(plm_t *self); +void plm_set_video_enabled(plm_t *self, int enabled); + + +// Get the number of video streams (0--1) reported in the system header. + +int plm_get_num_video_streams(plm_t *self); + + +// Get the display width/height of the video stream. + +int plm_get_width(plm_t *self); +int plm_get_height(plm_t *self); + + +// Get the framerate of the video stream in frames per second. + +double plm_get_framerate(plm_t *self); + + +// Get or set whether audio decoding is enabled. Default TRUE. + +int plm_get_audio_enabled(plm_t *self); +void plm_set_audio_enabled(plm_t *self, int enabled); + + +// Get the number of audio streams (0--4) reported in the system header. + +int plm_get_num_audio_streams(plm_t *self); + + +// Set the desired audio stream (0--3). Default 0. + +void plm_set_audio_stream(plm_t *self, int stream_index); + + +// Get the samplerate of the audio stream in samples per second. + +int plm_get_samplerate(plm_t *self); + + +// Get or set the audio lead time in seconds - the time in which audio samples +// are decoded in advance (or behind) the video decode time. Typically this +// should be set to the duration of the buffer of the audio API that you use +// for output. E.g. for SDL2: (SDL_AudioSpec.samples / samplerate) + +double plm_get_audio_lead_time(plm_t *self); +void plm_set_audio_lead_time(plm_t *self, double lead_time); + + +// Get the current internal time in seconds. + +double plm_get_time(plm_t *self); + + +// Get the video duration of the underlying source in seconds. + +double plm_get_duration(plm_t *self); + + +// Rewind all buffers back to the beginning. + +void plm_rewind(plm_t *self); + + +// Get or set looping. Default FALSE. + +int plm_get_loop(plm_t *self); +void plm_set_loop(plm_t *self, int loop); + + +// Get whether the file has ended. If looping is enabled, this will always +// return FALSE. + +int plm_has_ended(plm_t *self); + + +// Set the callback for decoded video frames used with plm_decode(). If no +// callback is set, video data will be ignored and not be decoded. The *user +// Parameter will be passed to your callback. + +void plm_set_video_decode_callback(plm_t *self, plm_video_decode_callback fp, void *user); + + +// Set the callback for decoded audio samples used with plm_decode(). If no +// callback is set, audio data will be ignored and not be decoded. The *user +// Parameter will be passed to your callback. + +void plm_set_audio_decode_callback(plm_t *self, plm_audio_decode_callback fp, void *user); + + +// Advance the internal timer by seconds and decode video/audio up to this time. +// This will call the video_decode_callback and audio_decode_callback any number +// of times. A frame-skip is not implemented, i.e. everything up to current time +// will be decoded. + +void plm_decode(plm_t *self, double seconds); + + +// Decode and return one video frame. Returns NULL if no frame could be decoded +// (either because the source ended or data is corrupt). If you only want to +// decode video, you should disable audio via plm_set_audio_enabled(). +// The returned plm_frame_t is valid until the next call to plm_decode_video() +// or until plm_destroy() is called. + +plm_frame_t *plm_decode_video(plm_t *self); + + +// Decode and return one audio frame. Returns NULL if no frame could be decoded +// (either because the source ended or data is corrupt). If you only want to +// decode audio, you should disable video via plm_set_video_enabled(). +// The returned plm_samples_t is valid until the next call to plm_decode_audio() +// or until plm_destroy() is called. + +plm_samples_t *plm_decode_audio(plm_t *self); + + +// Seek to the specified time, clamped between 0 -- duration. This can only be +// used when the underlying plm_buffer is seekable, i.e. for files, fixed +// memory buffers or _for_appending buffers. +// If seek_exact is TRUE this will seek to the exact time, otherwise it will +// seek to the last intra frame just before the desired time. Exact seeking can +// be slow, because all frames up to the seeked one have to be decoded on top of +// the previous intra frame. +// If seeking succeeds, this function will call the video_decode_callback +// exactly once with the target frame. If audio is enabled, it will also call +// the audio_decode_callback any number of times, until the audio_lead_time is +// satisfied. +// Returns TRUE if seeking succeeded or FALSE if no frame could be found. + +int plm_seek(plm_t *self, double time, int seek_exact); + + +// Similar to plm_seek(), but will not call the video_decode_callback, +// audio_decode_callback or make any attempts to sync audio. +// Returns the found frame or NULL if no frame could be found. + +plm_frame_t *plm_seek_frame(plm_t *self, double time, int seek_exact); + + + +// ----------------------------------------------------------------------------- +// plm_buffer public API +// Provides the data source for all other plm_* interfaces + + +// The default size for buffers created from files or by the high-level API + +#ifndef PLM_BUFFER_DEFAULT_SIZE +#define PLM_BUFFER_DEFAULT_SIZE (128 * 1024) +#endif + + +// Create a buffer instance with a filename. Returns NULL if the file could not +// be opened. + +plm_buffer_t *plm_buffer_create_with_filename(const char *filename); + + +// Create a buffer instance with a file handle. Pass TRUE to close_when_done +// to let plmpeg call fclose() on the handle when plm_destroy() is called. + +plm_buffer_t *plm_buffer_create_with_file(FILE *fh, int close_when_done); + + +// Create a buffer instance with a pointer to memory as source. This assumes +// the whole file is in memory. The bytes are not copied. Pass 1 to +// free_when_done to let plmpeg call free() on the pointer when plm_destroy() +// is called. + +plm_buffer_t *plm_buffer_create_with_memory(uint8_t *bytes, size_t length, int free_when_done); + + +// Create an empty buffer with an initial capacity. The buffer will grow +// as needed. Data that has already been read, will be discarded. + +plm_buffer_t *plm_buffer_create_with_capacity(size_t capacity); + + +// Create an empty buffer with an initial capacity. The buffer will grow +// as needed. Decoded data will *not* be discarded. This can be used when +// loading a file over the network, without needing to throttle the download. +// It also allows for seeking in the already loaded data. + +plm_buffer_t *plm_buffer_create_for_appending(size_t initial_capacity); + + +// Destroy a buffer instance and free all data + +void plm_buffer_destroy(plm_buffer_t *self); + + +// Copy data into the buffer. If the data to be written is larger than the +// available space, the buffer will realloc() with a larger capacity. +// Returns the number of bytes written. This will always be the same as the +// passed in length, except when the buffer was created _with_memory() for +// which _write() is forbidden. + +size_t plm_buffer_write(plm_buffer_t *self, uint8_t *bytes, size_t length); + + +// Mark the current byte length as the end of this buffer and signal that no +// more data is expected to be written to it. This function should be called +// just after the last plm_buffer_write(). +// For _with_capacity buffers, this is cleared on a plm_buffer_rewind(). + +void plm_buffer_signal_end(plm_buffer_t *self); + + +// Set a callback that is called whenever the buffer needs more data + +void plm_buffer_set_load_callback(plm_buffer_t *self, plm_buffer_load_callback fp, void *user); + + +// Rewind the buffer back to the beginning. When loading from a file handle, +// this also seeks to the beginning of the file. + +void plm_buffer_rewind(plm_buffer_t *self); + + +// Get the total size. For files, this returns the file size. For all other +// types it returns the number of bytes currently in the buffer. + +size_t plm_buffer_get_size(plm_buffer_t *self); + + +// Get the number of remaining (yet unread) bytes in the buffer. This can be +// useful to throttle writing. + +size_t plm_buffer_get_remaining(plm_buffer_t *self); + + +// Get whether the read position of the buffer is at the end and no more data +// is expected. + +int plm_buffer_has_ended(plm_buffer_t *self); + + + +// ----------------------------------------------------------------------------- +// plm_demux public API +// Demux an MPEG Program Stream (PS) data into separate packages + + +// Various Packet Types + +static const int PLM_DEMUX_PACKET_PRIVATE = 0xBD; +static const int PLM_DEMUX_PACKET_AUDIO_1 = 0xC0; +static const int PLM_DEMUX_PACKET_AUDIO_2 = 0xC1; +static const int PLM_DEMUX_PACKET_AUDIO_3 = 0xC2; +static const int PLM_DEMUX_PACKET_AUDIO_4 = 0xC2; +static const int PLM_DEMUX_PACKET_VIDEO_1 = 0xE0; + + +// Create a demuxer with a plm_buffer as source. This will also attempt to read +// the pack and system headers from the buffer. + +plm_demux_t *plm_demux_create(plm_buffer_t *buffer, int destroy_when_done); + + +// Destroy a demuxer and free all data. + +void plm_demux_destroy(plm_demux_t *self); + + +// Returns TRUE/FALSE whether pack and system headers have been found. This will +// attempt to read the headers if non are present yet. + +int plm_demux_has_headers(plm_demux_t *self); + + +// Returns the number of video streams found in the system header. This will +// attempt to read the system header if non is present yet. + +int plm_demux_get_num_video_streams(plm_demux_t *self); + + +// Returns the number of audio streams found in the system header. This will +// attempt to read the system header if non is present yet. + +int plm_demux_get_num_audio_streams(plm_demux_t *self); + + +// Rewind the internal buffer. See plm_buffer_rewind(). + +void plm_demux_rewind(plm_demux_t *self); + + +// Get whether the file has ended. This will be cleared on seeking or rewind. + +int plm_demux_has_ended(plm_demux_t *self); + + +// Seek to a packet of the specified type with a PTS just before specified time. +// If force_intra is TRUE, only packets containing an intra frame will be +// considered - this only makes sense when the type is PLM_DEMUX_PACKET_VIDEO_1. +// Note that the specified time is considered 0-based, regardless of the first +// PTS in the data source. + +plm_packet_t *plm_demux_seek(plm_demux_t *self, double time, int type, int force_intra); + + +// Get the PTS of the first packet of this type. Returns PLM_PACKET_INVALID_TS +// if not packet of this packet type can be found. + +double plm_demux_get_start_time(plm_demux_t *self, int type); + + +// Get the duration for the specified packet type - i.e. the span between the +// the first PTS and the last PTS in the data source. This only makes sense when +// the underlying data source is a file or fixed memory. + +double plm_demux_get_duration(plm_demux_t *self, int type); + + +// Decode and return the next packet. The returned packet_t is valid until +// the next call to plm_demux_decode() or until the demuxer is destroyed. + +plm_packet_t *plm_demux_decode(plm_demux_t *self); + + + +// ----------------------------------------------------------------------------- +// plm_video public API +// Decode MPEG1 Video ("mpeg1") data into raw YCrCb frames + + +// Create a video decoder with a plm_buffer as source. + +plm_video_t *plm_video_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done); + + +// Destroy a video decoder and free all data. + +void plm_video_destroy(plm_video_t *self); + + +// Get whether a sequence header was found and we can accurately report on +// dimensions and framerate. + +int plm_video_has_header(plm_video_t *self); + + +// Get the framerate in frames per second. + +double plm_video_get_framerate(plm_video_t *self); + + +// Get the display width/height. + +int plm_video_get_width(plm_video_t *self); +int plm_video_get_height(plm_video_t *self); + + +// Set "no delay" mode. When enabled, the decoder assumes that the video does +// *not* contain any B-Frames. This is useful for reducing lag when streaming. +// The default is FALSE. + +void plm_video_set_no_delay(plm_video_t *self, int no_delay); + + +// Get the current internal time in seconds. + +double plm_video_get_time(plm_video_t *self); + + +// Set the current internal time in seconds. This is only useful when you +// manipulate the underlying video buffer and want to enforce a correct +// timestamps. + +void plm_video_set_time(plm_video_t *self, double time); + + +// Rewind the internal buffer. See plm_buffer_rewind(). + +void plm_video_rewind(plm_video_t *self); + + +// Get whether the file has ended. This will be cleared on rewind. + +int plm_video_has_ended(plm_video_t *self); + + +// Decode and return one frame of video and advance the internal time by +// 1/framerate seconds. The returned frame_t is valid until the next call of +// plm_video_decode() or until the video decoder is destroyed. + +plm_frame_t *plm_video_decode(plm_video_t *self); + + +// Convert the YCrCb data of a frame into interleaved R G B data. The stride +// specifies the width in bytes of the destination buffer. I.e. the number of +// bytes from one line to the next. The stride must be at least +// (frame->width * bytes_per_pixel). The buffer pointed to by *dest must have a +// size of at least (stride * frame->height). +// Note that the alpha component of the dest buffer is always left untouched. + +void plm_frame_to_rgb(plm_frame_t *frame, uint8_t *dest, int stride); +void plm_frame_to_bgr(plm_frame_t *frame, uint8_t *dest, int stride); +void plm_frame_to_rgba(plm_frame_t *frame, uint8_t *dest, int stride); +void plm_frame_to_bgra(plm_frame_t *frame, uint8_t *dest, int stride); +void plm_frame_to_argb(plm_frame_t *frame, uint8_t *dest, int stride); +void plm_frame_to_abgr(plm_frame_t *frame, uint8_t *dest, int stride); + + +// ----------------------------------------------------------------------------- +// plm_audio public API +// Decode MPEG-1 Audio Layer II ("mp2") data into raw samples + + +// Create an audio decoder with a plm_buffer as source. + +plm_audio_t *plm_audio_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done); + + +// Destroy an audio decoder and free all data. + +void plm_audio_destroy(plm_audio_t *self); + + +// Get whether a frame header was found and we can accurately report on +// samplerate. + +int plm_audio_has_header(plm_audio_t *self); + + +// Get the samplerate in samples per second. + +int plm_audio_get_samplerate(plm_audio_t *self); + + +// Get the current internal time in seconds. + +double plm_audio_get_time(plm_audio_t *self); + + +// Set the current internal time in seconds. This is only useful when you +// manipulate the underlying video buffer and want to enforce a correct +// timestamps. + +void plm_audio_set_time(plm_audio_t *self, double time); + + +// Rewind the internal buffer. See plm_buffer_rewind(). + +void plm_audio_rewind(plm_audio_t *self); + + +// Get whether the file has ended. This will be cleared on rewind. + +int plm_audio_has_ended(plm_audio_t *self); + + +// Decode and return one "frame" of audio and advance the internal time by +// (PLM_AUDIO_SAMPLES_PER_FRAME/samplerate) seconds. The returned samples_t +// is valid until the next call of plm_audio_decode() or until the audio +// decoder is destroyed. + +plm_samples_t *plm_audio_decode(plm_audio_t *self); + + + +#ifdef __cplusplus +} +#endif + +#endif // PL_MPEG_H + + + + + +// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- +// IMPLEMENTATION + +#ifdef PL_MPEG_IMPLEMENTATION + +#include +#include + +#ifndef TRUE +#define TRUE 1 +#define FALSE 0 +#endif + +#define PLM_UNUSED(expr) (void)(expr) + + +// ----------------------------------------------------------------------------- +// plm (high-level interface) implementation + +struct plm_t { + plm_demux_t *demux; + double time; + int has_ended; + int loop; + int has_decoders; + + int video_enabled; + int video_packet_type; + plm_buffer_t *video_buffer; + plm_video_t *video_decoder; + + int audio_enabled; + int audio_stream_index; + int audio_packet_type; + double audio_lead_time; + plm_buffer_t *audio_buffer; + plm_audio_t *audio_decoder; + + plm_video_decode_callback video_decode_callback; + void *video_decode_callback_user_data; + + plm_audio_decode_callback audio_decode_callback; + void *audio_decode_callback_user_data; +}; + +int plm_init_decoders(plm_t *self); +void plm_handle_end(plm_t *self); +void plm_read_video_packet(plm_buffer_t *buffer, void *user); +void plm_read_audio_packet(plm_buffer_t *buffer, void *user); +void plm_read_packets(plm_t *self, int requested_type); + +plm_t *plm_create_with_filename(const char *filename) { + plm_buffer_t *buffer = plm_buffer_create_with_filename(filename); + if (!buffer) { + return NULL; + } + return plm_create_with_buffer(buffer, TRUE); +} + +plm_t *plm_create_with_file(FILE *fh, int close_when_done) { + plm_buffer_t *buffer = plm_buffer_create_with_file(fh, close_when_done); + return plm_create_with_buffer(buffer, TRUE); +} + +plm_t *plm_create_with_memory(uint8_t *bytes, size_t length, int free_when_done) { + plm_buffer_t *buffer = plm_buffer_create_with_memory(bytes, length, free_when_done); + return plm_create_with_buffer(buffer, TRUE); +} + +plm_t *plm_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done) { + plm_t *self = (plm_t *)malloc(sizeof(plm_t)); + memset(self, 0, sizeof(plm_t)); + + self->demux = plm_demux_create(buffer, destroy_when_done); + self->video_enabled = TRUE; + self->audio_enabled = TRUE; + plm_init_decoders(self); + + return self; +} + +int plm_init_decoders(plm_t *self) { + if (self->has_decoders) { + return TRUE; + } + + if (!plm_demux_has_headers(self->demux)) { + return FALSE; + } + + if (plm_demux_get_num_video_streams(self->demux) > 0) { + if (self->video_enabled) { + self->video_packet_type = PLM_DEMUX_PACKET_VIDEO_1; + } + self->video_buffer = plm_buffer_create_with_capacity(PLM_BUFFER_DEFAULT_SIZE); + plm_buffer_set_load_callback(self->video_buffer, plm_read_video_packet, self); + } + + if (plm_demux_get_num_audio_streams(self->demux) > 0) { + if (self->audio_enabled) { + self->audio_packet_type = PLM_DEMUX_PACKET_AUDIO_1 + self->audio_stream_index; + } + self->audio_buffer = plm_buffer_create_with_capacity(PLM_BUFFER_DEFAULT_SIZE); + plm_buffer_set_load_callback(self->audio_buffer, plm_read_audio_packet, self); + } + + if (self->video_buffer) { + self->video_decoder = plm_video_create_with_buffer(self->video_buffer, TRUE); + } + + if (self->audio_buffer) { + self->audio_decoder = plm_audio_create_with_buffer(self->audio_buffer, TRUE); + } + + self->has_decoders = TRUE; + return TRUE; +} + +void plm_destroy(plm_t *self) { + if (self->video_decoder) { + plm_video_destroy(self->video_decoder); + } + if (self->audio_decoder) { + plm_audio_destroy(self->audio_decoder); + } + + plm_demux_destroy(self->demux); + free(self); +} + +int plm_get_audio_enabled(plm_t *self) { + return self->audio_enabled; +} + +int plm_has_headers(plm_t *self) { + if (!plm_demux_has_headers(self->demux)) { + return FALSE; + } + + if (!plm_init_decoders(self)) { + return FALSE; + } + + if ( + (self->video_decoder && !plm_video_has_header(self->video_decoder)) || + (self->audio_decoder && !plm_audio_has_header(self->audio_decoder)) + ) { + return FALSE; + } + + return TRUE; +} + +void plm_set_audio_enabled(plm_t *self, int enabled) { + self->audio_enabled = enabled; + + if (!enabled) { + self->audio_packet_type = 0; + return; + } + + self->audio_packet_type = (plm_init_decoders(self) && self->audio_decoder) + ? PLM_DEMUX_PACKET_AUDIO_1 + self->audio_stream_index + : 0; +} + +void plm_set_audio_stream(plm_t *self, int stream_index) { + if (stream_index < 0 || stream_index > 3) { + return; + } + self->audio_stream_index = stream_index; + + // Set the correct audio_packet_type + plm_set_audio_enabled(self, self->audio_enabled); +} + +int plm_get_video_enabled(plm_t *self) { + return self->video_enabled; +} + +void plm_set_video_enabled(plm_t *self, int enabled) { + self->video_enabled = enabled; + + if (!enabled) { + self->video_packet_type = 0; + return; + } + + self->video_packet_type = (plm_init_decoders(self) && self->video_decoder) + ? PLM_DEMUX_PACKET_VIDEO_1 + : 0; +} + +int plm_get_num_video_streams(plm_t *self) { + return plm_demux_get_num_video_streams(self->demux); +} + +int plm_get_width(plm_t *self) { + return (plm_init_decoders(self) && self->video_decoder) + ? plm_video_get_width(self->video_decoder) + : 0; +} + +int plm_get_height(plm_t *self) { + return (plm_init_decoders(self) && self->video_decoder) + ? plm_video_get_height(self->video_decoder) + : 0; +} + +double plm_get_framerate(plm_t *self) { + return (plm_init_decoders(self) && self->video_decoder) + ? plm_video_get_framerate(self->video_decoder) + : 0; +} + +int plm_get_num_audio_streams(plm_t *self) { + return plm_demux_get_num_audio_streams(self->demux); +} + +int plm_get_samplerate(plm_t *self) { + return (plm_init_decoders(self) && self->audio_decoder) + ? plm_audio_get_samplerate(self->audio_decoder) + : 0; +} + +double plm_get_audio_lead_time(plm_t *self) { + return self->audio_lead_time; +} + +void plm_set_audio_lead_time(plm_t *self, double lead_time) { + self->audio_lead_time = lead_time; +} + +double plm_get_time(plm_t *self) { + return self->time; +} + +double plm_get_duration(plm_t *self) { + return plm_demux_get_duration(self->demux, PLM_DEMUX_PACKET_VIDEO_1); +} + +void plm_rewind(plm_t *self) { + if (self->video_decoder) { + plm_video_rewind(self->video_decoder); + } + + if (self->audio_decoder) { + plm_audio_rewind(self->audio_decoder); + } + + plm_demux_rewind(self->demux); + self->time = 0; +} + +int plm_get_loop(plm_t *self) { + return self->loop; +} + +void plm_set_loop(plm_t *self, int loop) { + self->loop = loop; +} + +int plm_has_ended(plm_t *self) { + return self->has_ended; +} + +void plm_set_video_decode_callback(plm_t *self, plm_video_decode_callback fp, void *user) { + self->video_decode_callback = fp; + self->video_decode_callback_user_data = user; +} + +void plm_set_audio_decode_callback(plm_t *self, plm_audio_decode_callback fp, void *user) { + self->audio_decode_callback = fp; + self->audio_decode_callback_user_data = user; +} + +void plm_decode(plm_t *self, double tick) { + if (!plm_init_decoders(self)) { + return; + } + + int decode_video = (self->video_decode_callback && self->video_packet_type); + int decode_audio = (self->audio_decode_callback && self->audio_packet_type); + + if (!decode_video && !decode_audio) { + // Nothing to do here + return; + } + + int did_decode = FALSE; + int decode_video_failed = FALSE; + int decode_audio_failed = FALSE; + + double video_target_time = self->time + tick; + double audio_target_time = self->time + tick + self->audio_lead_time; + + do { + did_decode = FALSE; + + if (decode_video && plm_video_get_time(self->video_decoder) < video_target_time) { + plm_frame_t *frame = plm_video_decode(self->video_decoder); + if (frame) { + self->video_decode_callback(self, frame, self->video_decode_callback_user_data); + did_decode = TRUE; + } + else { + decode_video_failed = TRUE; + } + } + + if (decode_audio && plm_audio_get_time(self->audio_decoder) < audio_target_time) { + plm_samples_t *samples = plm_audio_decode(self->audio_decoder); + if (samples) { + self->audio_decode_callback(self, samples, self->audio_decode_callback_user_data); + did_decode = TRUE; + } + else { + decode_audio_failed = TRUE; + } + } + } while (did_decode); + + // Did all sources we wanted to decode fail and the demuxer is at the end? + if ( + (!decode_video || decode_video_failed) && + (!decode_audio || decode_audio_failed) && + plm_demux_has_ended(self->demux) + ) { + plm_handle_end(self); + return; + } + + self->time += tick; +} + +plm_frame_t *plm_decode_video(plm_t *self) { + if (!plm_init_decoders(self)) { + return NULL; + } + + if (!self->video_packet_type) { + return NULL; + } + + plm_frame_t *frame = plm_video_decode(self->video_decoder); + if (frame) { + self->time = frame->time; + } + else if (plm_demux_has_ended(self->demux)) { + plm_handle_end(self); + } + return frame; +} + +plm_samples_t *plm_decode_audio(plm_t *self) { + if (!plm_init_decoders(self)) { + return NULL; + } + + if (!self->audio_packet_type) { + return NULL; + } + + plm_samples_t *samples = plm_audio_decode(self->audio_decoder); + if (samples) { + self->time = samples->time; + } + else if (plm_demux_has_ended(self->demux)) { + plm_handle_end(self); + } + return samples; +} + +void plm_handle_end(plm_t *self) { + if (self->loop) { + plm_rewind(self); + } + else { + self->has_ended = TRUE; + } +} + +void plm_read_video_packet(plm_buffer_t *buffer, void *user) { + PLM_UNUSED(buffer); + plm_t *self = (plm_t *)user; + plm_read_packets(self, self->video_packet_type); +} + +void plm_read_audio_packet(plm_buffer_t *buffer, void *user) { + PLM_UNUSED(buffer); + plm_t *self = (plm_t *)user; + plm_read_packets(self, self->audio_packet_type); +} + +void plm_read_packets(plm_t *self, int requested_type) { + plm_packet_t *packet; + while ((packet = plm_demux_decode(self->demux))) { + if (packet->type == self->video_packet_type) { + plm_buffer_write(self->video_buffer, packet->data, packet->length); + } + else if (packet->type == self->audio_packet_type) { + plm_buffer_write(self->audio_buffer, packet->data, packet->length); + } + + if (packet->type == requested_type) { + return; + } + } + + if (plm_demux_has_ended(self->demux)) { + if (self->video_buffer) { + plm_buffer_signal_end(self->video_buffer); + } + if (self->audio_buffer) { + plm_buffer_signal_end(self->audio_buffer); + } + } +} + +plm_frame_t *plm_seek_frame(plm_t *self, double time, int seek_exact) { + if (!plm_init_decoders(self)) { + return NULL; + } + + if (!self->video_packet_type) { + return NULL; + } + + int type = self->video_packet_type; + + double start_time = plm_demux_get_start_time(self->demux, type); + double duration = plm_demux_get_duration(self->demux, type); + + if (time < 0) { + time = 0; + } + else if (time > duration) { + time = duration; + } + + plm_packet_t *packet = plm_demux_seek(self->demux, time, type, TRUE); + if (!packet) { + return NULL; + } + + // Disable writing to the audio buffer while decoding video + int previous_audio_packet_type = self->audio_packet_type; + self->audio_packet_type = 0; + + // Clear video buffer and decode the found packet + plm_video_rewind(self->video_decoder); + plm_video_set_time(self->video_decoder, packet->pts - start_time); + plm_buffer_write(self->video_buffer, packet->data, packet->length); + plm_frame_t *frame = plm_video_decode(self->video_decoder); + + // If we want to seek to an exact frame, we have to decode all frames + // on top of the intra frame we just jumped to. + if (seek_exact) { + while (frame && frame->time < time) { + frame = plm_video_decode(self->video_decoder); + } + } + + // Enable writing to the audio buffer again? + self->audio_packet_type = previous_audio_packet_type; + + if (frame) { + self->time = frame->time; + } + + self->has_ended = FALSE; + return frame; +} + +int plm_seek(plm_t *self, double time, int seek_exact) { + plm_frame_t *frame = plm_seek_frame(self, time, seek_exact); + + if (!frame) { + return FALSE; + } + + if (self->video_decode_callback) { + self->video_decode_callback(self, frame, self->video_decode_callback_user_data); + } + + // If audio is not enabled we are done here. + if (!self->audio_packet_type) { + return TRUE; + } + + // Sync up Audio. This demuxes more packets until the first audio packet + // with a PTS greater than the current time is found. plm_decode() is then + // called to decode enough audio data to satisfy the audio_lead_time. + + double start_time = plm_demux_get_start_time(self->demux, self->video_packet_type); + plm_audio_rewind(self->audio_decoder); + + plm_packet_t *packet = NULL; + while ((packet = plm_demux_decode(self->demux))) { + if (packet->type == self->video_packet_type) { + plm_buffer_write(self->video_buffer, packet->data, packet->length); + } + else if ( + packet->type == self->audio_packet_type && + packet->pts - start_time > self->time + ) { + plm_audio_set_time(self->audio_decoder, packet->pts - start_time); + plm_buffer_write(self->audio_buffer, packet->data, packet->length); + plm_decode(self, 0); + break; + } + } + + return TRUE; +} + + + +// ----------------------------------------------------------------------------- +// plm_buffer implementation + +enum plm_buffer_mode { + PLM_BUFFER_MODE_FILE, + PLM_BUFFER_MODE_FIXED_MEM, + PLM_BUFFER_MODE_RING, + PLM_BUFFER_MODE_APPEND +}; + +struct plm_buffer_t { + size_t bit_index; + size_t capacity; + size_t length; + size_t total_size; + int discard_read_bytes; + int has_ended; + int free_when_done; + int close_when_done; + FILE *fh; + plm_buffer_load_callback load_callback; + void *load_callback_user_data; + uint8_t *bytes; + enum plm_buffer_mode mode; +}; + +typedef struct { + int16_t index; + int16_t value; +} plm_vlc_t; + +typedef struct { + int16_t index; + uint16_t value; +} plm_vlc_uint_t; + + +void plm_buffer_seek(plm_buffer_t *self, size_t pos); +size_t plm_buffer_tell(plm_buffer_t *self); +void plm_buffer_discard_read_bytes(plm_buffer_t *self); +void plm_buffer_load_file_callback(plm_buffer_t *self, void *user); + +int plm_buffer_has(plm_buffer_t *self, size_t count); +int plm_buffer_read(plm_buffer_t *self, int count); +void plm_buffer_align(plm_buffer_t *self); +void plm_buffer_skip(plm_buffer_t *self, size_t count); +int plm_buffer_skip_bytes(plm_buffer_t *self, uint8_t v); +int plm_buffer_next_start_code(plm_buffer_t *self); +int plm_buffer_find_start_code(plm_buffer_t *self, int code); +int plm_buffer_no_start_code(plm_buffer_t *self); +int16_t plm_buffer_read_vlc(plm_buffer_t *self, const plm_vlc_t *table); +uint16_t plm_buffer_read_vlc_uint(plm_buffer_t *self, const plm_vlc_uint_t *table); + +plm_buffer_t *plm_buffer_create_with_filename(const char *filename) { + FILE *fh = fopen(filename, "rb"); + if (!fh) { + return NULL; + } + return plm_buffer_create_with_file(fh, TRUE); +} + +plm_buffer_t *plm_buffer_create_with_file(FILE *fh, int close_when_done) { + plm_buffer_t *self = plm_buffer_create_with_capacity(PLM_BUFFER_DEFAULT_SIZE); + self->fh = fh; + self->close_when_done = close_when_done; + self->mode = PLM_BUFFER_MODE_FILE; + self->discard_read_bytes = TRUE; + + fseek(self->fh, 0, SEEK_END); + self->total_size = ftell(self->fh); + fseek(self->fh, 0, SEEK_SET); + + plm_buffer_set_load_callback(self, plm_buffer_load_file_callback, NULL); + return self; +} + +plm_buffer_t *plm_buffer_create_with_memory(uint8_t *bytes, size_t length, int free_when_done) { + plm_buffer_t *self = (plm_buffer_t *)malloc(sizeof(plm_buffer_t)); + memset(self, 0, sizeof(plm_buffer_t)); + self->capacity = length; + self->length = length; + self->total_size = length; + self->free_when_done = free_when_done; + self->bytes = bytes; + self->mode = PLM_BUFFER_MODE_FIXED_MEM; + self->discard_read_bytes = FALSE; + return self; +} + +plm_buffer_t *plm_buffer_create_with_capacity(size_t capacity) { + plm_buffer_t *self = (plm_buffer_t *)malloc(sizeof(plm_buffer_t)); + memset(self, 0, sizeof(plm_buffer_t)); + self->capacity = capacity; + self->free_when_done = TRUE; + self->bytes = (uint8_t *)malloc(capacity); + self->mode = PLM_BUFFER_MODE_RING; + self->discard_read_bytes = TRUE; + return self; +} + +plm_buffer_t *plm_buffer_create_for_appending(size_t initial_capacity) { + plm_buffer_t *self = plm_buffer_create_with_capacity(initial_capacity); + self->mode = PLM_BUFFER_MODE_APPEND; + self->discard_read_bytes = FALSE; + return self; +} + +void plm_buffer_destroy(plm_buffer_t *self) { + if (self->fh && self->close_when_done) { + fclose(self->fh); + } + if (self->free_when_done) { + free(self->bytes); + } + free(self); +} + +size_t plm_buffer_get_size(plm_buffer_t *self) { + return (self->mode == PLM_BUFFER_MODE_FILE) + ? self->total_size + : self->length; +} + +size_t plm_buffer_get_remaining(plm_buffer_t *self) { + return self->length - (self->bit_index >> 3); +} + +size_t plm_buffer_write(plm_buffer_t *self, uint8_t *bytes, size_t length) { + if (self->mode == PLM_BUFFER_MODE_FIXED_MEM) { + return 0; + } + + if (self->discard_read_bytes) { + // This should be a ring buffer, but instead it just shifts all unread + // data to the beginning of the buffer and appends new data at the end. + // Seems to be good enough. + + plm_buffer_discard_read_bytes(self); + if (self->mode == PLM_BUFFER_MODE_RING) { + self->total_size = 0; + } + } + + // Do we have to resize to fit the new data? + size_t bytes_available = self->capacity - self->length; + if (bytes_available < length) { + size_t new_size = self->capacity; + do { + new_size *= 2; + } while (new_size - self->length < length); + self->bytes = (uint8_t *)realloc(self->bytes, new_size); + self->capacity = new_size; + } + + memcpy(self->bytes + self->length, bytes, length); + self->length += length; + self->has_ended = FALSE; + return length; +} + +void plm_buffer_signal_end(plm_buffer_t *self) { + self->total_size = self->length; +} + +void plm_buffer_set_load_callback(plm_buffer_t *self, plm_buffer_load_callback fp, void *user) { + self->load_callback = fp; + self->load_callback_user_data = user; +} + +void plm_buffer_rewind(plm_buffer_t *self) { + plm_buffer_seek(self, 0); +} + +void plm_buffer_seek(plm_buffer_t *self, size_t pos) { + self->has_ended = FALSE; + + if (self->mode == PLM_BUFFER_MODE_FILE) { + fseek(self->fh, pos, SEEK_SET); + self->bit_index = 0; + self->length = 0; + } + else if (self->mode == PLM_BUFFER_MODE_RING) { + if (pos != 0) { + // Seeking to non-0 is forbidden for dynamic-mem buffers + return; + } + self->bit_index = 0; + self->length = 0; + self->total_size = 0; + } + else if (pos < self->length) { + self->bit_index = pos << 3; + } +} + +size_t plm_buffer_tell(plm_buffer_t *self) { + return self->mode == PLM_BUFFER_MODE_FILE + ? ftell(self->fh) + (self->bit_index >> 3) - self->length + : self->bit_index >> 3; +} + +void plm_buffer_discard_read_bytes(plm_buffer_t *self) { + size_t byte_pos = self->bit_index >> 3; + if (byte_pos == self->length) { + self->bit_index = 0; + self->length = 0; + } + else if (byte_pos > 0) { + memmove(self->bytes, self->bytes + byte_pos, self->length - byte_pos); + self->bit_index -= byte_pos << 3; + self->length -= byte_pos; + } +} + +void plm_buffer_load_file_callback(plm_buffer_t *self, void *user) { + PLM_UNUSED(user); + + if (self->discard_read_bytes) { + plm_buffer_discard_read_bytes(self); + } + + size_t bytes_available = self->capacity - self->length; + size_t bytes_read = fread(self->bytes + self->length, 1, bytes_available, self->fh); + self->length += bytes_read; + + if (bytes_read == 0) { + self->has_ended = TRUE; + } +} + +int plm_buffer_has_ended(plm_buffer_t *self) { + return self->has_ended; +} + +int plm_buffer_has(plm_buffer_t *self, size_t count) { + if (((self->length << 3) - self->bit_index) >= count) { + return TRUE; + } + + if (self->load_callback) { + self->load_callback(self, self->load_callback_user_data); + + if (((self->length << 3) - self->bit_index) >= count) { + return TRUE; + } + } + + if (self->total_size != 0 && self->length == self->total_size) { + self->has_ended = TRUE; + } + return FALSE; +} + +int plm_buffer_read(plm_buffer_t *self, int count) { + if (!plm_buffer_has(self, count)) { + return 0; + } + + int value = 0; + while (count) { + int current_byte = self->bytes[self->bit_index >> 3]; + + int remaining = 8 - (self->bit_index & 7); // Remaining bits in byte + int read = remaining < count ? remaining : count; // Bits in self run + int shift = remaining - read; + int mask = (0xff >> (8 - read)); + + value = (value << read) | ((current_byte & (mask << shift)) >> shift); + + self->bit_index += read; + count -= read; + } + + return value; +} + +void plm_buffer_align(plm_buffer_t *self) { + self->bit_index = ((self->bit_index + 7) >> 3) << 3; // Align to next byte +} + +void plm_buffer_skip(plm_buffer_t *self, size_t count) { + if (plm_buffer_has(self, count)) { + self->bit_index += count; + } +} + +int plm_buffer_skip_bytes(plm_buffer_t *self, uint8_t v) { + plm_buffer_align(self); + int skipped = 0; + while (plm_buffer_has(self, 8) && self->bytes[self->bit_index >> 3] == v) { + self->bit_index += 8; + skipped++; + } + return skipped; +} + +int plm_buffer_next_start_code(plm_buffer_t *self) { + plm_buffer_align(self); + + while (plm_buffer_has(self, (5 << 3))) { + size_t byte_index = (self->bit_index) >> 3; + if ( + self->bytes[byte_index] == 0x00 && + self->bytes[byte_index + 1] == 0x00 && + self->bytes[byte_index + 2] == 0x01 + ) { + self->bit_index = (byte_index + 4) << 3; + return self->bytes[byte_index + 3]; + } + self->bit_index += 8; + } + return -1; +} + +int plm_buffer_find_start_code(plm_buffer_t *self, int code) { + int current = 0; + while (TRUE) { + current = plm_buffer_next_start_code(self); + if (current == code || current == -1) { + return current; + } + } + return -1; +} + +int plm_buffer_has_start_code(plm_buffer_t *self, int code) { + size_t previous_bit_index = self->bit_index; + int previous_discard_read_bytes = self->discard_read_bytes; + + self->discard_read_bytes = FALSE; + int current = plm_buffer_find_start_code(self, code); + + self->bit_index = previous_bit_index; + self->discard_read_bytes = previous_discard_read_bytes; + return current; +} + +int plm_buffer_peek_non_zero(plm_buffer_t *self, int bit_count) { + if (!plm_buffer_has(self, bit_count)) { + return FALSE; + } + + int val = plm_buffer_read(self, bit_count); + self->bit_index -= bit_count; + return val != 0; +} + +int16_t plm_buffer_read_vlc(plm_buffer_t *self, const plm_vlc_t *table) { + plm_vlc_t state = {0, 0}; + do { + state = table[state.index + plm_buffer_read(self, 1)]; + } while (state.index > 0); + return state.value; +} + +uint16_t plm_buffer_read_vlc_uint(plm_buffer_t *self, const plm_vlc_uint_t *table) { + return (uint16_t)plm_buffer_read_vlc(self, (const plm_vlc_t *)table); +} + + + +// ---------------------------------------------------------------------------- +// plm_demux implementation + +static const int PLM_START_PACK = 0xBA; +static const int PLM_START_END = 0xB9; +static const int PLM_START_SYSTEM = 0xBB; + +struct plm_demux_t { + plm_buffer_t *buffer; + int destroy_buffer_when_done; + double system_clock_ref; + + size_t last_file_size; + double last_decoded_pts; + double start_time; + double duration; + + int start_code; + int has_pack_header; + int has_system_header; + int has_headers; + + int num_audio_streams; + int num_video_streams; + plm_packet_t current_packet; + plm_packet_t next_packet; +}; + + +void plm_demux_buffer_seek(plm_demux_t *self, size_t pos); +double plm_demux_decode_time(plm_demux_t *self); +plm_packet_t *plm_demux_decode_packet(plm_demux_t *self, int type); +plm_packet_t *plm_demux_get_packet(plm_demux_t *self); + +plm_demux_t *plm_demux_create(plm_buffer_t *buffer, int destroy_when_done) { + plm_demux_t *self = (plm_demux_t *)malloc(sizeof(plm_demux_t)); + memset(self, 0, sizeof(plm_demux_t)); + + self->buffer = buffer; + self->destroy_buffer_when_done = destroy_when_done; + + self->start_time = PLM_PACKET_INVALID_TS; + self->duration = PLM_PACKET_INVALID_TS; + self->start_code = -1; + + plm_demux_has_headers(self); + return self; +} + +void plm_demux_destroy(plm_demux_t *self) { + if (self->destroy_buffer_when_done) { + plm_buffer_destroy(self->buffer); + } + free(self); +} + +int plm_demux_has_headers(plm_demux_t *self) { + if (self->has_headers) { + return TRUE; + } + + // Decode pack header + if (!self->has_pack_header) { + if ( + self->start_code != PLM_START_PACK && + plm_buffer_find_start_code(self->buffer, PLM_START_PACK) == -1 + ) { + return FALSE; + } + + self->start_code = PLM_START_PACK; + if (!plm_buffer_has(self->buffer, 64)) { + return FALSE; + } + self->start_code = -1; + + if (plm_buffer_read(self->buffer, 4) != 0x02) { + return FALSE; + } + + self->system_clock_ref = plm_demux_decode_time(self); + plm_buffer_skip(self->buffer, 1); + plm_buffer_skip(self->buffer, 22); // mux_rate * 50 + plm_buffer_skip(self->buffer, 1); + + self->has_pack_header = TRUE; + } + + // Decode system header + if (!self->has_system_header) { + if ( + self->start_code != PLM_START_SYSTEM && + plm_buffer_find_start_code(self->buffer, PLM_START_SYSTEM) == -1 + ) { + return FALSE; + } + + self->start_code = PLM_START_SYSTEM; + if (!plm_buffer_has(self->buffer, 56)) { + return FALSE; + } + self->start_code = -1; + + plm_buffer_skip(self->buffer, 16); // header_length + plm_buffer_skip(self->buffer, 24); // rate bound + self->num_audio_streams = plm_buffer_read(self->buffer, 6); + plm_buffer_skip(self->buffer, 5); // misc flags + self->num_video_streams = plm_buffer_read(self->buffer, 5); + + self->has_system_header = TRUE; + } + + self->has_headers = TRUE; + return TRUE; +} + +int plm_demux_get_num_video_streams(plm_demux_t *self) { + return plm_demux_has_headers(self) + ? self->num_video_streams + : 0; +} + +int plm_demux_get_num_audio_streams(plm_demux_t *self) { + return plm_demux_has_headers(self) + ? self->num_audio_streams + : 0; +} + +void plm_demux_rewind(plm_demux_t *self) { + plm_buffer_rewind(self->buffer); + self->current_packet.length = 0; + self->next_packet.length = 0; + self->start_code = -1; +} + +int plm_demux_has_ended(plm_demux_t *self) { + return plm_buffer_has_ended(self->buffer); +} + +void plm_demux_buffer_seek(plm_demux_t *self, size_t pos) { + plm_buffer_seek(self->buffer, pos); + self->current_packet.length = 0; + self->next_packet.length = 0; + self->start_code = -1; +} + +double plm_demux_get_start_time(plm_demux_t *self, int type) { + if (self->start_time != PLM_PACKET_INVALID_TS) { + return self->start_time; + } + + int previous_pos = plm_buffer_tell(self->buffer); + int previous_start_code = self->start_code; + + // Find first video PTS + plm_demux_rewind(self); + do { + plm_packet_t *packet = plm_demux_decode(self); + if (!packet) { + break; + } + if (packet->type == type) { + self->start_time = packet->pts; + } + } while (self->start_time == PLM_PACKET_INVALID_TS); + + plm_demux_buffer_seek(self, previous_pos); + self->start_code = previous_start_code; + return self->start_time; +} + +double plm_demux_get_duration(plm_demux_t *self, int type) { + size_t file_size = plm_buffer_get_size(self->buffer); + + if ( + self->duration != PLM_PACKET_INVALID_TS && + self->last_file_size == file_size + ) { + return self->duration; + } + + size_t previous_pos = plm_buffer_tell(self->buffer); + int previous_start_code = self->start_code; + + // Find last video PTS. Start searching 64kb from the end and go further + // back if needed. + long start_range = 64 * 1024; + long max_range = 4096 * 1024; + for (long range = start_range; range <= max_range; range *= 2) { + long seek_pos = file_size - range; + if (seek_pos < 0) { + seek_pos = 0; + range = max_range; // Make sure to bail after this round + } + plm_demux_buffer_seek(self, seek_pos); + self->current_packet.length = 0; + + double last_pts = PLM_PACKET_INVALID_TS; + plm_packet_t *packet = NULL; + while ((packet = plm_demux_decode(self))) { + if (packet->pts != PLM_PACKET_INVALID_TS && packet->type == type) { + last_pts = packet->pts; + } + } + if (last_pts != PLM_PACKET_INVALID_TS) { + self->duration = last_pts - plm_demux_get_start_time(self, type); + break; + } + } + + plm_demux_buffer_seek(self, previous_pos); + self->start_code = previous_start_code; + self->last_file_size = file_size; + return self->duration; +} + +plm_packet_t *plm_demux_seek(plm_demux_t *self, double seek_time, int type, int force_intra) { + if (!plm_demux_has_headers(self)) { + return NULL; + } + + // Using the current time, current byte position and the average bytes per + // second for this file, try to jump to a byte position that hopefully has + // packets containing timestamps within one second before to the desired + // seek_time. + + // If we hit close to the seek_time scan through all packets to find the + // last one (just before the seek_time) containing an intra frame. + // Otherwise we should at least be closer than before. Calculate the bytes + // per second for the jumped range and jump again. + + // The number of retries here is hard-limited to a generous amount. Usually + // the correct range is found after 1--5 jumps, even for files with very + // variable bitrates. If significantly more jumps are needed, there's + // probably something wrong with the file and we just avoid getting into an + // infinite loop. 32 retries should be enough for anybody. + + double duration = plm_demux_get_duration(self, type); + long file_size = plm_buffer_get_size(self->buffer); + long byterate = file_size / duration; + + double cur_time = self->last_decoded_pts; + double scan_span = 1; + + if (seek_time > duration) { + seek_time = duration; + } + else if (seek_time < 0) { + seek_time = 0; + } + seek_time += self->start_time; + + for (int retry = 0; retry < 32; retry++) { + int found_packet_with_pts = FALSE; + int found_packet_in_range = FALSE; + long last_valid_packet_start = -1; + double first_packet_time = PLM_PACKET_INVALID_TS; + + long cur_pos = plm_buffer_tell(self->buffer); + + // Estimate byte offset and jump to it. + long offset = (seek_time - cur_time - scan_span) * byterate; + long seek_pos = cur_pos + offset; + if (seek_pos < 0) { + seek_pos = 0; + } + else if (seek_pos > file_size - 256) { + seek_pos = file_size - 256; + } + + plm_demux_buffer_seek(self, seek_pos); + + // Scan through all packets up to the seek_time to find the last packet + // containing an intra frame. + while (plm_buffer_find_start_code(self->buffer, type) != -1) { + long packet_start = plm_buffer_tell(self->buffer); + plm_packet_t *packet = plm_demux_decode_packet(self, type); + + // Skip packet if it has no PTS + if (!packet || packet->pts == PLM_PACKET_INVALID_TS) { + continue; + } + + // Bail scanning through packets if we hit one that is outside + // seek_time - scan_span. + // We also adjust the cur_time and byterate values here so the next + // iteration can be a bit more precise. + if (packet->pts > seek_time || packet->pts < seek_time - scan_span) { + found_packet_with_pts = TRUE; + byterate = (seek_pos - cur_pos) / (packet->pts - cur_time); + cur_time = packet->pts; + break; + } + + // If we are still here, it means this packet is in close range to + // the seek_time. If this is the first packet for this jump position + // record the PTS. If we later have to back off, when there was no + // intra frame in this range, we can lower the seek_time to not scan + // this range again. + if (!found_packet_in_range) { + found_packet_in_range = TRUE; + first_packet_time = packet->pts; + } + + // Check if this is an intra frame packet. If so, record the buffer + // position of the start of this packet. We want to jump back to it + // later, when we know it's the last intra frame before desired + // seek time. + if (force_intra) { + for (size_t i = 0; i < packet->length - 6; i++) { + // Find the START_PICTURE code + if ( + packet->data[i] == 0x00 && + packet->data[i + 1] == 0x00 && + packet->data[i + 2] == 0x01 && + packet->data[i + 3] == 0x00 + ) { + // Bits 11--13 in the picture header contain the frame + // type, where 1=Intra + if ((packet->data[i + 5] & 0x38) == 8) { + last_valid_packet_start = packet_start; + } + break; + } + } + } + + // If we don't want intra frames, just use the last PTS found. + else { + last_valid_packet_start = packet_start; + } + } + + // If there was at least one intra frame in the range scanned above, + // our search is over. Jump back to the packet and decode it again. + if (last_valid_packet_start != -1) { + plm_demux_buffer_seek(self, last_valid_packet_start); + return plm_demux_decode_packet(self, type); + } + + // If we hit the right range, but still found no intra frame, we have + // to increases the scan_span. This is done exponentially to also handle + // video files with very few intra frames. + else if (found_packet_in_range) { + scan_span *= 2; + seek_time = first_packet_time; + } + + // If we didn't find any packet with a PTS, it probably means we reached + // the end of the file. Estimate byterate and cur_time accordingly. + else if (!found_packet_with_pts) { + byterate = (seek_pos - cur_pos) / (duration - cur_time); + cur_time = duration; + } + } + + return NULL; +} + +plm_packet_t *plm_demux_decode(plm_demux_t *self) { + if (!plm_demux_has_headers(self)) { + return NULL; + } + + if (self->current_packet.length) { + size_t bits_till_next_packet = self->current_packet.length << 3; + if (!plm_buffer_has(self->buffer, bits_till_next_packet)) { + return NULL; + } + plm_buffer_skip(self->buffer, bits_till_next_packet); + self->current_packet.length = 0; + } + + // Pending packet waiting for data? + if (self->next_packet.length) { + return plm_demux_get_packet(self); + } + + // Pending packet waiting for header? + if (self->start_code != -1) { + return plm_demux_decode_packet(self, self->start_code); + } + + do { + self->start_code = plm_buffer_next_start_code(self->buffer); + if ( + self->start_code == PLM_DEMUX_PACKET_VIDEO_1 || + self->start_code == PLM_DEMUX_PACKET_PRIVATE || ( + self->start_code >= PLM_DEMUX_PACKET_AUDIO_1 && + self->start_code <= PLM_DEMUX_PACKET_AUDIO_4 + ) + ) { + return plm_demux_decode_packet(self, self->start_code); + } + } while (self->start_code != -1); + + return NULL; +} + +double plm_demux_decode_time(plm_demux_t *self) { + int64_t clock = plm_buffer_read(self->buffer, 3) << 30; + plm_buffer_skip(self->buffer, 1); + clock |= plm_buffer_read(self->buffer, 15) << 15; + plm_buffer_skip(self->buffer, 1); + clock |= plm_buffer_read(self->buffer, 15); + plm_buffer_skip(self->buffer, 1); + return (double)clock / 90000.0; +} + +plm_packet_t *plm_demux_decode_packet(plm_demux_t *self, int type) { + if (!plm_buffer_has(self->buffer, 16 << 3)) { + return NULL; + } + + self->start_code = -1; + + self->next_packet.type = type; + self->next_packet.length = plm_buffer_read(self->buffer, 16); + self->next_packet.length -= plm_buffer_skip_bytes(self->buffer, 0xff); // stuffing + + // skip P-STD + if (plm_buffer_read(self->buffer, 2) == 0x01) { + plm_buffer_skip(self->buffer, 16); + self->next_packet.length -= 2; + } + + int pts_dts_marker = plm_buffer_read(self->buffer, 2); + if (pts_dts_marker == 0x03) { + self->next_packet.pts = plm_demux_decode_time(self); + self->last_decoded_pts = self->next_packet.pts; + plm_buffer_skip(self->buffer, 40); // skip dts + self->next_packet.length -= 10; + } + else if (pts_dts_marker == 0x02) { + self->next_packet.pts = plm_demux_decode_time(self); + self->last_decoded_pts = self->next_packet.pts; + self->next_packet.length -= 5; + } + else if (pts_dts_marker == 0x00) { + self->next_packet.pts = PLM_PACKET_INVALID_TS; + plm_buffer_skip(self->buffer, 4); + self->next_packet.length -= 1; + } + else { + return NULL; // invalid + } + + return plm_demux_get_packet(self); +} + +plm_packet_t *plm_demux_get_packet(plm_demux_t *self) { + if (!plm_buffer_has(self->buffer, self->next_packet.length << 3)) { + return NULL; + } + + self->current_packet.data = self->buffer->bytes + (self->buffer->bit_index >> 3); + self->current_packet.length = self->next_packet.length; + self->current_packet.type = self->next_packet.type; + self->current_packet.pts = self->next_packet.pts; + + self->next_packet.length = 0; + return &self->current_packet; +} + + + +// ----------------------------------------------------------------------------- +// plm_video implementation + +// Inspired by Java MPEG-1 Video Decoder and Player by Zoltan Korandi +// https://sourceforge.net/projects/javampeg1video/ + +static const int PLM_VIDEO_PICTURE_TYPE_INTRA = 1; +static const int PLM_VIDEO_PICTURE_TYPE_PREDICTIVE = 2; +static const int PLM_VIDEO_PICTURE_TYPE_B = 3; + +static const int PLM_START_SEQUENCE = 0xB3; +static const int PLM_START_SLICE_FIRST = 0x01; +static const int PLM_START_SLICE_LAST = 0xAF; +static const int PLM_START_PICTURE = 0x00; +static const int PLM_START_EXTENSION = 0xB5; +static const int PLM_START_USER_DATA = 0xB2; + +#define PLM_START_IS_SLICE(c) \ + (c >= PLM_START_SLICE_FIRST && c <= PLM_START_SLICE_LAST) + +static const double PLM_VIDEO_PICTURE_RATE[] = { + 0.000, 23.976, 24.000, 25.000, 29.970, 30.000, 50.000, 59.940, + 60.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000 +}; + +static const uint8_t PLM_VIDEO_ZIG_ZAG[] = { + 0, 1, 8, 16, 9, 2, 3, 10, + 17, 24, 32, 25, 18, 11, 4, 5, + 12, 19, 26, 33, 40, 48, 41, 34, + 27, 20, 13, 6, 7, 14, 21, 28, + 35, 42, 49, 56, 57, 50, 43, 36, + 29, 22, 15, 23, 30, 37, 44, 51, + 58, 59, 52, 45, 38, 31, 39, 46, + 53, 60, 61, 54, 47, 55, 62, 63 +}; + +static const uint8_t PLM_VIDEO_INTRA_QUANT_MATRIX[] = { + 8, 16, 19, 22, 26, 27, 29, 34, + 16, 16, 22, 24, 27, 29, 34, 37, + 19, 22, 26, 27, 29, 34, 34, 38, + 22, 22, 26, 27, 29, 34, 37, 40, + 22, 26, 27, 29, 32, 35, 40, 48, + 26, 27, 29, 32, 35, 40, 48, 58, + 26, 27, 29, 34, 38, 46, 56, 69, + 27, 29, 35, 38, 46, 56, 69, 83 +}; + +static const uint8_t PLM_VIDEO_NON_INTRA_QUANT_MATRIX[] = { + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16 +}; + +static const uint8_t PLM_VIDEO_PREMULTIPLIER_MATRIX[] = { + 32, 44, 42, 38, 32, 25, 17, 9, + 44, 62, 58, 52, 44, 35, 24, 12, + 42, 58, 55, 49, 42, 33, 23, 12, + 38, 52, 49, 44, 38, 30, 20, 10, + 32, 44, 42, 38, 32, 25, 17, 9, + 25, 35, 33, 30, 25, 20, 14, 7, + 17, 24, 23, 20, 17, 14, 9, 5, + 9, 12, 12, 10, 9, 7, 5, 2 +}; + +static const plm_vlc_t PLM_VIDEO_MACROBLOCK_ADDRESS_INCREMENT[] = { + { 1 << 1, 0}, { 0, 1}, // 0: x + { 2 << 1, 0}, { 3 << 1, 0}, // 1: 0x + { 4 << 1, 0}, { 5 << 1, 0}, // 2: 00x + { 0, 3}, { 0, 2}, // 3: 01x + { 6 << 1, 0}, { 7 << 1, 0}, // 4: 000x + { 0, 5}, { 0, 4}, // 5: 001x + { 8 << 1, 0}, { 9 << 1, 0}, // 6: 0000x + { 0, 7}, { 0, 6}, // 7: 0001x + { 10 << 1, 0}, { 11 << 1, 0}, // 8: 0000 0x + { 12 << 1, 0}, { 13 << 1, 0}, // 9: 0000 1x + { 14 << 1, 0}, { 15 << 1, 0}, // 10: 0000 00x + { 16 << 1, 0}, { 17 << 1, 0}, // 11: 0000 01x + { 18 << 1, 0}, { 19 << 1, 0}, // 12: 0000 10x + { 0, 9}, { 0, 8}, // 13: 0000 11x + { -1, 0}, { 20 << 1, 0}, // 14: 0000 000x + { -1, 0}, { 21 << 1, 0}, // 15: 0000 001x + { 22 << 1, 0}, { 23 << 1, 0}, // 16: 0000 010x + { 0, 15}, { 0, 14}, // 17: 0000 011x + { 0, 13}, { 0, 12}, // 18: 0000 100x + { 0, 11}, { 0, 10}, // 19: 0000 101x + { 24 << 1, 0}, { 25 << 1, 0}, // 20: 0000 0001x + { 26 << 1, 0}, { 27 << 1, 0}, // 21: 0000 0011x + { 28 << 1, 0}, { 29 << 1, 0}, // 22: 0000 0100x + { 30 << 1, 0}, { 31 << 1, 0}, // 23: 0000 0101x + { 32 << 1, 0}, { -1, 0}, // 24: 0000 0001 0x + { -1, 0}, { 33 << 1, 0}, // 25: 0000 0001 1x + { 34 << 1, 0}, { 35 << 1, 0}, // 26: 0000 0011 0x + { 36 << 1, 0}, { 37 << 1, 0}, // 27: 0000 0011 1x + { 38 << 1, 0}, { 39 << 1, 0}, // 28: 0000 0100 0x + { 0, 21}, { 0, 20}, // 29: 0000 0100 1x + { 0, 19}, { 0, 18}, // 30: 0000 0101 0x + { 0, 17}, { 0, 16}, // 31: 0000 0101 1x + { 0, 35}, { -1, 0}, // 32: 0000 0001 00x + { -1, 0}, { 0, 34}, // 33: 0000 0001 11x + { 0, 33}, { 0, 32}, // 34: 0000 0011 00x + { 0, 31}, { 0, 30}, // 35: 0000 0011 01x + { 0, 29}, { 0, 28}, // 36: 0000 0011 10x + { 0, 27}, { 0, 26}, // 37: 0000 0011 11x + { 0, 25}, { 0, 24}, // 38: 0000 0100 00x + { 0, 23}, { 0, 22}, // 39: 0000 0100 01x +}; + +static const plm_vlc_t PLM_VIDEO_MACROBLOCK_TYPE_INTRA[] = { + { 1 << 1, 0}, { 0, 0x01}, // 0: x + { -1, 0}, { 0, 0x11}, // 1: 0x +}; + +static const plm_vlc_t PLM_VIDEO_MACROBLOCK_TYPE_PREDICTIVE[] = { + { 1 << 1, 0}, { 0, 0x0a}, // 0: x + { 2 << 1, 0}, { 0, 0x02}, // 1: 0x + { 3 << 1, 0}, { 0, 0x08}, // 2: 00x + { 4 << 1, 0}, { 5 << 1, 0}, // 3: 000x + { 6 << 1, 0}, { 0, 0x12}, // 4: 0000x + { 0, 0x1a}, { 0, 0x01}, // 5: 0001x + { -1, 0}, { 0, 0x11}, // 6: 0000 0x +}; + +static const plm_vlc_t PLM_VIDEO_MACROBLOCK_TYPE_B[] = { + { 1 << 1, 0}, { 2 << 1, 0}, // 0: x + { 3 << 1, 0}, { 4 << 1, 0}, // 1: 0x + { 0, 0x0c}, { 0, 0x0e}, // 2: 1x + { 5 << 1, 0}, { 6 << 1, 0}, // 3: 00x + { 0, 0x04}, { 0, 0x06}, // 4: 01x + { 7 << 1, 0}, { 8 << 1, 0}, // 5: 000x + { 0, 0x08}, { 0, 0x0a}, // 6: 001x + { 9 << 1, 0}, { 10 << 1, 0}, // 7: 0000x + { 0, 0x1e}, { 0, 0x01}, // 8: 0001x + { -1, 0}, { 0, 0x11}, // 9: 0000 0x + { 0, 0x16}, { 0, 0x1a}, // 10: 0000 1x +}; + +static const plm_vlc_t *PLM_VIDEO_MACROBLOCK_TYPE[] = { + NULL, + PLM_VIDEO_MACROBLOCK_TYPE_INTRA, + PLM_VIDEO_MACROBLOCK_TYPE_PREDICTIVE, + PLM_VIDEO_MACROBLOCK_TYPE_B +}; + +static const plm_vlc_t PLM_VIDEO_CODE_BLOCK_PATTERN[] = { + { 1 << 1, 0}, { 2 << 1, 0}, // 0: x + { 3 << 1, 0}, { 4 << 1, 0}, // 1: 0x + { 5 << 1, 0}, { 6 << 1, 0}, // 2: 1x + { 7 << 1, 0}, { 8 << 1, 0}, // 3: 00x + { 9 << 1, 0}, { 10 << 1, 0}, // 4: 01x + { 11 << 1, 0}, { 12 << 1, 0}, // 5: 10x + { 13 << 1, 0}, { 0, 60}, // 6: 11x + { 14 << 1, 0}, { 15 << 1, 0}, // 7: 000x + { 16 << 1, 0}, { 17 << 1, 0}, // 8: 001x + { 18 << 1, 0}, { 19 << 1, 0}, // 9: 010x + { 20 << 1, 0}, { 21 << 1, 0}, // 10: 011x + { 22 << 1, 0}, { 23 << 1, 0}, // 11: 100x + { 0, 32}, { 0, 16}, // 12: 101x + { 0, 8}, { 0, 4}, // 13: 110x + { 24 << 1, 0}, { 25 << 1, 0}, // 14: 0000x + { 26 << 1, 0}, { 27 << 1, 0}, // 15: 0001x + { 28 << 1, 0}, { 29 << 1, 0}, // 16: 0010x + { 30 << 1, 0}, { 31 << 1, 0}, // 17: 0011x + { 0, 62}, { 0, 2}, // 18: 0100x + { 0, 61}, { 0, 1}, // 19: 0101x + { 0, 56}, { 0, 52}, // 20: 0110x + { 0, 44}, { 0, 28}, // 21: 0111x + { 0, 40}, { 0, 20}, // 22: 1000x + { 0, 48}, { 0, 12}, // 23: 1001x + { 32 << 1, 0}, { 33 << 1, 0}, // 24: 0000 0x + { 34 << 1, 0}, { 35 << 1, 0}, // 25: 0000 1x + { 36 << 1, 0}, { 37 << 1, 0}, // 26: 0001 0x + { 38 << 1, 0}, { 39 << 1, 0}, // 27: 0001 1x + { 40 << 1, 0}, { 41 << 1, 0}, // 28: 0010 0x + { 42 << 1, 0}, { 43 << 1, 0}, // 29: 0010 1x + { 0, 63}, { 0, 3}, // 30: 0011 0x + { 0, 36}, { 0, 24}, // 31: 0011 1x + { 44 << 1, 0}, { 45 << 1, 0}, // 32: 0000 00x + { 46 << 1, 0}, { 47 << 1, 0}, // 33: 0000 01x + { 48 << 1, 0}, { 49 << 1, 0}, // 34: 0000 10x + { 50 << 1, 0}, { 51 << 1, 0}, // 35: 0000 11x + { 52 << 1, 0}, { 53 << 1, 0}, // 36: 0001 00x + { 54 << 1, 0}, { 55 << 1, 0}, // 37: 0001 01x + { 56 << 1, 0}, { 57 << 1, 0}, // 38: 0001 10x + { 58 << 1, 0}, { 59 << 1, 0}, // 39: 0001 11x + { 0, 34}, { 0, 18}, // 40: 0010 00x + { 0, 10}, { 0, 6}, // 41: 0010 01x + { 0, 33}, { 0, 17}, // 42: 0010 10x + { 0, 9}, { 0, 5}, // 43: 0010 11x + { -1, 0}, { 60 << 1, 0}, // 44: 0000 000x + { 61 << 1, 0}, { 62 << 1, 0}, // 45: 0000 001x + { 0, 58}, { 0, 54}, // 46: 0000 010x + { 0, 46}, { 0, 30}, // 47: 0000 011x + { 0, 57}, { 0, 53}, // 48: 0000 100x + { 0, 45}, { 0, 29}, // 49: 0000 101x + { 0, 38}, { 0, 26}, // 50: 0000 110x + { 0, 37}, { 0, 25}, // 51: 0000 111x + { 0, 43}, { 0, 23}, // 52: 0001 000x + { 0, 51}, { 0, 15}, // 53: 0001 001x + { 0, 42}, { 0, 22}, // 54: 0001 010x + { 0, 50}, { 0, 14}, // 55: 0001 011x + { 0, 41}, { 0, 21}, // 56: 0001 100x + { 0, 49}, { 0, 13}, // 57: 0001 101x + { 0, 35}, { 0, 19}, // 58: 0001 110x + { 0, 11}, { 0, 7}, // 59: 0001 111x + { 0, 39}, { 0, 27}, // 60: 0000 0001x + { 0, 59}, { 0, 55}, // 61: 0000 0010x + { 0, 47}, { 0, 31}, // 62: 0000 0011x +}; + +static const plm_vlc_t PLM_VIDEO_MOTION[] = { + { 1 << 1, 0}, { 0, 0}, // 0: x + { 2 << 1, 0}, { 3 << 1, 0}, // 1: 0x + { 4 << 1, 0}, { 5 << 1, 0}, // 2: 00x + { 0, 1}, { 0, -1}, // 3: 01x + { 6 << 1, 0}, { 7 << 1, 0}, // 4: 000x + { 0, 2}, { 0, -2}, // 5: 001x + { 8 << 1, 0}, { 9 << 1, 0}, // 6: 0000x + { 0, 3}, { 0, -3}, // 7: 0001x + { 10 << 1, 0}, { 11 << 1, 0}, // 8: 0000 0x + { 12 << 1, 0}, { 13 << 1, 0}, // 9: 0000 1x + { -1, 0}, { 14 << 1, 0}, // 10: 0000 00x + { 15 << 1, 0}, { 16 << 1, 0}, // 11: 0000 01x + { 17 << 1, 0}, { 18 << 1, 0}, // 12: 0000 10x + { 0, 4}, { 0, -4}, // 13: 0000 11x + { -1, 0}, { 19 << 1, 0}, // 14: 0000 001x + { 20 << 1, 0}, { 21 << 1, 0}, // 15: 0000 010x + { 0, 7}, { 0, -7}, // 16: 0000 011x + { 0, 6}, { 0, -6}, // 17: 0000 100x + { 0, 5}, { 0, -5}, // 18: 0000 101x + { 22 << 1, 0}, { 23 << 1, 0}, // 19: 0000 0011x + { 24 << 1, 0}, { 25 << 1, 0}, // 20: 0000 0100x + { 26 << 1, 0}, { 27 << 1, 0}, // 21: 0000 0101x + { 28 << 1, 0}, { 29 << 1, 0}, // 22: 0000 0011 0x + { 30 << 1, 0}, { 31 << 1, 0}, // 23: 0000 0011 1x + { 32 << 1, 0}, { 33 << 1, 0}, // 24: 0000 0100 0x + { 0, 10}, { 0, -10}, // 25: 0000 0100 1x + { 0, 9}, { 0, -9}, // 26: 0000 0101 0x + { 0, 8}, { 0, -8}, // 27: 0000 0101 1x + { 0, 16}, { 0, -16}, // 28: 0000 0011 00x + { 0, 15}, { 0, -15}, // 29: 0000 0011 01x + { 0, 14}, { 0, -14}, // 30: 0000 0011 10x + { 0, 13}, { 0, -13}, // 31: 0000 0011 11x + { 0, 12}, { 0, -12}, // 32: 0000 0100 00x + { 0, 11}, { 0, -11}, // 33: 0000 0100 01x +}; + +static const plm_vlc_t PLM_VIDEO_DCT_SIZE_LUMINANCE[] = { + { 1 << 1, 0}, { 2 << 1, 0}, // 0: x + { 0, 1}, { 0, 2}, // 1: 0x + { 3 << 1, 0}, { 4 << 1, 0}, // 2: 1x + { 0, 0}, { 0, 3}, // 3: 10x + { 0, 4}, { 5 << 1, 0}, // 4: 11x + { 0, 5}, { 6 << 1, 0}, // 5: 111x + { 0, 6}, { 7 << 1, 0}, // 6: 1111x + { 0, 7}, { 8 << 1, 0}, // 7: 1111 1x + { 0, 8}, { -1, 0}, // 8: 1111 11x +}; + +static const plm_vlc_t PLM_VIDEO_DCT_SIZE_CHROMINANCE[] = { + { 1 << 1, 0}, { 2 << 1, 0}, // 0: x + { 0, 0}, { 0, 1}, // 1: 0x + { 0, 2}, { 3 << 1, 0}, // 2: 1x + { 0, 3}, { 4 << 1, 0}, // 3: 11x + { 0, 4}, { 5 << 1, 0}, // 4: 111x + { 0, 5}, { 6 << 1, 0}, // 5: 1111x + { 0, 6}, { 7 << 1, 0}, // 6: 1111 1x + { 0, 7}, { 8 << 1, 0}, // 7: 1111 11x + { 0, 8}, { -1, 0}, // 8: 1111 111x +}; + +static const plm_vlc_t *PLM_VIDEO_DCT_SIZE[] = { + PLM_VIDEO_DCT_SIZE_LUMINANCE, + PLM_VIDEO_DCT_SIZE_CHROMINANCE, + PLM_VIDEO_DCT_SIZE_CHROMINANCE +}; + + +// dct_coeff bitmap: +// 0xff00 run +// 0x00ff level + +// Decoded values are unsigned. Sign bit follows in the stream. + +static const plm_vlc_uint_t PLM_VIDEO_DCT_COEFF[] = { + { 1 << 1, 0}, { 0, 0x0001}, // 0: x + { 2 << 1, 0}, { 3 << 1, 0}, // 1: 0x + { 4 << 1, 0}, { 5 << 1, 0}, // 2: 00x + { 6 << 1, 0}, { 0, 0x0101}, // 3: 01x + { 7 << 1, 0}, { 8 << 1, 0}, // 4: 000x + { 9 << 1, 0}, { 10 << 1, 0}, // 5: 001x + { 0, 0x0002}, { 0, 0x0201}, // 6: 010x + { 11 << 1, 0}, { 12 << 1, 0}, // 7: 0000x + { 13 << 1, 0}, { 14 << 1, 0}, // 8: 0001x + { 15 << 1, 0}, { 0, 0x0003}, // 9: 0010x + { 0, 0x0401}, { 0, 0x0301}, // 10: 0011x + { 16 << 1, 0}, { 0, 0xffff}, // 11: 0000 0x + { 17 << 1, 0}, { 18 << 1, 0}, // 12: 0000 1x + { 0, 0x0701}, { 0, 0x0601}, // 13: 0001 0x + { 0, 0x0102}, { 0, 0x0501}, // 14: 0001 1x + { 19 << 1, 0}, { 20 << 1, 0}, // 15: 0010 0x + { 21 << 1, 0}, { 22 << 1, 0}, // 16: 0000 00x + { 0, 0x0202}, { 0, 0x0901}, // 17: 0000 10x + { 0, 0x0004}, { 0, 0x0801}, // 18: 0000 11x + { 23 << 1, 0}, { 24 << 1, 0}, // 19: 0010 00x + { 25 << 1, 0}, { 26 << 1, 0}, // 20: 0010 01x + { 27 << 1, 0}, { 28 << 1, 0}, // 21: 0000 000x + { 29 << 1, 0}, { 30 << 1, 0}, // 22: 0000 001x + { 0, 0x0d01}, { 0, 0x0006}, // 23: 0010 000x + { 0, 0x0c01}, { 0, 0x0b01}, // 24: 0010 001x + { 0, 0x0302}, { 0, 0x0103}, // 25: 0010 010x + { 0, 0x0005}, { 0, 0x0a01}, // 26: 0010 011x + { 31 << 1, 0}, { 32 << 1, 0}, // 27: 0000 0000x + { 33 << 1, 0}, { 34 << 1, 0}, // 28: 0000 0001x + { 35 << 1, 0}, { 36 << 1, 0}, // 29: 0000 0010x + { 37 << 1, 0}, { 38 << 1, 0}, // 30: 0000 0011x + { 39 << 1, 0}, { 40 << 1, 0}, // 31: 0000 0000 0x + { 41 << 1, 0}, { 42 << 1, 0}, // 32: 0000 0000 1x + { 43 << 1, 0}, { 44 << 1, 0}, // 33: 0000 0001 0x + { 45 << 1, 0}, { 46 << 1, 0}, // 34: 0000 0001 1x + { 0, 0x1001}, { 0, 0x0502}, // 35: 0000 0010 0x + { 0, 0x0007}, { 0, 0x0203}, // 36: 0000 0010 1x + { 0, 0x0104}, { 0, 0x0f01}, // 37: 0000 0011 0x + { 0, 0x0e01}, { 0, 0x0402}, // 38: 0000 0011 1x + { 47 << 1, 0}, { 48 << 1, 0}, // 39: 0000 0000 00x + { 49 << 1, 0}, { 50 << 1, 0}, // 40: 0000 0000 01x + { 51 << 1, 0}, { 52 << 1, 0}, // 41: 0000 0000 10x + { 53 << 1, 0}, { 54 << 1, 0}, // 42: 0000 0000 11x + { 55 << 1, 0}, { 56 << 1, 0}, // 43: 0000 0001 00x + { 57 << 1, 0}, { 58 << 1, 0}, // 44: 0000 0001 01x + { 59 << 1, 0}, { 60 << 1, 0}, // 45: 0000 0001 10x + { 61 << 1, 0}, { 62 << 1, 0}, // 46: 0000 0001 11x + { -1, 0}, { 63 << 1, 0}, // 47: 0000 0000 000x + { 64 << 1, 0}, { 65 << 1, 0}, // 48: 0000 0000 001x + { 66 << 1, 0}, { 67 << 1, 0}, // 49: 0000 0000 010x + { 68 << 1, 0}, { 69 << 1, 0}, // 50: 0000 0000 011x + { 70 << 1, 0}, { 71 << 1, 0}, // 51: 0000 0000 100x + { 72 << 1, 0}, { 73 << 1, 0}, // 52: 0000 0000 101x + { 74 << 1, 0}, { 75 << 1, 0}, // 53: 0000 0000 110x + { 76 << 1, 0}, { 77 << 1, 0}, // 54: 0000 0000 111x + { 0, 0x000b}, { 0, 0x0802}, // 55: 0000 0001 000x + { 0, 0x0403}, { 0, 0x000a}, // 56: 0000 0001 001x + { 0, 0x0204}, { 0, 0x0702}, // 57: 0000 0001 010x + { 0, 0x1501}, { 0, 0x1401}, // 58: 0000 0001 011x + { 0, 0x0009}, { 0, 0x1301}, // 59: 0000 0001 100x + { 0, 0x1201}, { 0, 0x0105}, // 60: 0000 0001 101x + { 0, 0x0303}, { 0, 0x0008}, // 61: 0000 0001 110x + { 0, 0x0602}, { 0, 0x1101}, // 62: 0000 0001 111x + { 78 << 1, 0}, { 79 << 1, 0}, // 63: 0000 0000 0001x + { 80 << 1, 0}, { 81 << 1, 0}, // 64: 0000 0000 0010x + { 82 << 1, 0}, { 83 << 1, 0}, // 65: 0000 0000 0011x + { 84 << 1, 0}, { 85 << 1, 0}, // 66: 0000 0000 0100x + { 86 << 1, 0}, { 87 << 1, 0}, // 67: 0000 0000 0101x + { 88 << 1, 0}, { 89 << 1, 0}, // 68: 0000 0000 0110x + { 90 << 1, 0}, { 91 << 1, 0}, // 69: 0000 0000 0111x + { 0, 0x0a02}, { 0, 0x0902}, // 70: 0000 0000 1000x + { 0, 0x0503}, { 0, 0x0304}, // 71: 0000 0000 1001x + { 0, 0x0205}, { 0, 0x0107}, // 72: 0000 0000 1010x + { 0, 0x0106}, { 0, 0x000f}, // 73: 0000 0000 1011x + { 0, 0x000e}, { 0, 0x000d}, // 74: 0000 0000 1100x + { 0, 0x000c}, { 0, 0x1a01}, // 75: 0000 0000 1101x + { 0, 0x1901}, { 0, 0x1801}, // 76: 0000 0000 1110x + { 0, 0x1701}, { 0, 0x1601}, // 77: 0000 0000 1111x + { 92 << 1, 0}, { 93 << 1, 0}, // 78: 0000 0000 0001 0x + { 94 << 1, 0}, { 95 << 1, 0}, // 79: 0000 0000 0001 1x + { 96 << 1, 0}, { 97 << 1, 0}, // 80: 0000 0000 0010 0x + { 98 << 1, 0}, { 99 << 1, 0}, // 81: 0000 0000 0010 1x + {100 << 1, 0}, {101 << 1, 0}, // 82: 0000 0000 0011 0x + {102 << 1, 0}, {103 << 1, 0}, // 83: 0000 0000 0011 1x + { 0, 0x001f}, { 0, 0x001e}, // 84: 0000 0000 0100 0x + { 0, 0x001d}, { 0, 0x001c}, // 85: 0000 0000 0100 1x + { 0, 0x001b}, { 0, 0x001a}, // 86: 0000 0000 0101 0x + { 0, 0x0019}, { 0, 0x0018}, // 87: 0000 0000 0101 1x + { 0, 0x0017}, { 0, 0x0016}, // 88: 0000 0000 0110 0x + { 0, 0x0015}, { 0, 0x0014}, // 89: 0000 0000 0110 1x + { 0, 0x0013}, { 0, 0x0012}, // 90: 0000 0000 0111 0x + { 0, 0x0011}, { 0, 0x0010}, // 91: 0000 0000 0111 1x + {104 << 1, 0}, {105 << 1, 0}, // 92: 0000 0000 0001 00x + {106 << 1, 0}, {107 << 1, 0}, // 93: 0000 0000 0001 01x + {108 << 1, 0}, {109 << 1, 0}, // 94: 0000 0000 0001 10x + {110 << 1, 0}, {111 << 1, 0}, // 95: 0000 0000 0001 11x + { 0, 0x0028}, { 0, 0x0027}, // 96: 0000 0000 0010 00x + { 0, 0x0026}, { 0, 0x0025}, // 97: 0000 0000 0010 01x + { 0, 0x0024}, { 0, 0x0023}, // 98: 0000 0000 0010 10x + { 0, 0x0022}, { 0, 0x0021}, // 99: 0000 0000 0010 11x + { 0, 0x0020}, { 0, 0x010e}, // 100: 0000 0000 0011 00x + { 0, 0x010d}, { 0, 0x010c}, // 101: 0000 0000 0011 01x + { 0, 0x010b}, { 0, 0x010a}, // 102: 0000 0000 0011 10x + { 0, 0x0109}, { 0, 0x0108}, // 103: 0000 0000 0011 11x + { 0, 0x0112}, { 0, 0x0111}, // 104: 0000 0000 0001 000x + { 0, 0x0110}, { 0, 0x010f}, // 105: 0000 0000 0001 001x + { 0, 0x0603}, { 0, 0x1002}, // 106: 0000 0000 0001 010x + { 0, 0x0f02}, { 0, 0x0e02}, // 107: 0000 0000 0001 011x + { 0, 0x0d02}, { 0, 0x0c02}, // 108: 0000 0000 0001 100x + { 0, 0x0b02}, { 0, 0x1f01}, // 109: 0000 0000 0001 101x + { 0, 0x1e01}, { 0, 0x1d01}, // 110: 0000 0000 0001 110x + { 0, 0x1c01}, { 0, 0x1b01}, // 111: 0000 0000 0001 111x +}; + +typedef struct { + int full_px; + int is_set; + int r_size; + int h; + int v; +} plm_video_motion_t; + +struct plm_video_t { + double framerate; + double time; + int frames_decoded; + int width; + int height; + int mb_width; + int mb_height; + int mb_size; + + int luma_width; + int luma_height; + + int chroma_width; + int chroma_height; + + int start_code; + int picture_type; + + plm_video_motion_t motion_forward; + plm_video_motion_t motion_backward; + + int has_sequence_header; + + int quantizer_scale; + int slice_begin; + int macroblock_address; + + int mb_row; + int mb_col; + + int macroblock_type; + int macroblock_intra; + + int dc_predictor[3]; + + plm_buffer_t *buffer; + int destroy_buffer_when_done; + + plm_frame_t frame_current; + plm_frame_t frame_forward; + plm_frame_t frame_backward; + + uint8_t *frames_data; + + int block_data[64]; + uint8_t intra_quant_matrix[64]; + uint8_t non_intra_quant_matrix[64]; + + int has_reference_frame; + int assume_no_b_frames; +}; + +static inline uint8_t plm_clamp(int n) { + if (n > 255) { + n = 255; + } + else if (n < 0) { + n = 0; + } + return n; +} + +int plm_video_decode_sequence_header(plm_video_t *self); +void plm_video_init_frame(plm_video_t *self, plm_frame_t *frame, uint8_t *base); +void plm_video_decode_picture(plm_video_t *self); +void plm_video_decode_slice(plm_video_t *self, int slice); +void plm_video_decode_macroblock(plm_video_t *self); +void plm_video_decode_motion_vectors(plm_video_t *self); +int plm_video_decode_motion_vector(plm_video_t *self, int r_size, int motion); +void plm_video_predict_macroblock(plm_video_t *self); +void plm_video_copy_macroblock(plm_video_t *self, plm_frame_t *s, int motion_h, int motion_v); +void plm_video_interpolate_macroblock(plm_video_t *self, plm_frame_t *s, int motion_h, int motion_v); +void plm_video_process_macroblock(plm_video_t *self, uint8_t *s, uint8_t *d, int mh, int mb, int bs, int interp); +void plm_video_decode_block(plm_video_t *self, int block); +void plm_video_idct(int *block); + +plm_video_t * plm_video_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done) { + plm_video_t *self = (plm_video_t *)malloc(sizeof(plm_video_t)); + memset(self, 0, sizeof(plm_video_t)); + + self->buffer = buffer; + self->destroy_buffer_when_done = destroy_when_done; + + // Attempt to decode the sequence header + self->start_code = plm_buffer_find_start_code(self->buffer, PLM_START_SEQUENCE); + if (self->start_code != -1) { + plm_video_decode_sequence_header(self); + } + return self; +} + +void plm_video_destroy(plm_video_t *self) { + if (self->destroy_buffer_when_done) { + plm_buffer_destroy(self->buffer); + } + + if (self->has_sequence_header) { + free(self->frames_data); + } + + free(self); +} + +double plm_video_get_framerate(plm_video_t *self) { + return plm_video_has_header(self) + ? self->framerate + : 0; +} + +int plm_video_get_width(plm_video_t *self) { + return plm_video_has_header(self) + ? self->width + : 0; +} + +int plm_video_get_height(plm_video_t *self) { + return plm_video_has_header(self) + ? self->height + : 0; +} + +void plm_video_set_no_delay(plm_video_t *self, int no_delay) { + self->assume_no_b_frames = no_delay; +} + +double plm_video_get_time(plm_video_t *self) { + return self->time; +} + +void plm_video_set_time(plm_video_t *self, double time) { + self->frames_decoded = self->framerate * time; + self->time = time; +} + +void plm_video_rewind(plm_video_t *self) { + plm_buffer_rewind(self->buffer); + self->time = 0; + self->frames_decoded = 0; + self->has_reference_frame = FALSE; + self->start_code = -1; +} + +int plm_video_has_ended(plm_video_t *self) { + return plm_buffer_has_ended(self->buffer); +} + +plm_frame_t *plm_video_decode(plm_video_t *self) { + if (!plm_video_has_header(self)) { + return NULL; + } + + plm_frame_t *frame = NULL; + do { + if (self->start_code != PLM_START_PICTURE) { + self->start_code = plm_buffer_find_start_code(self->buffer, PLM_START_PICTURE); + + if (self->start_code == -1) { + // If we reached the end of the file and the previously decoded + // frame was a reference frame, we still have to return it. + if ( + self->has_reference_frame && + !self->assume_no_b_frames && + plm_buffer_has_ended(self->buffer) && ( + self->picture_type == PLM_VIDEO_PICTURE_TYPE_INTRA || + self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE + ) + ) { + self->has_reference_frame = FALSE; + frame = &self->frame_backward; + break; + } + + return NULL; + } + } + + // Make sure we have a full picture in the buffer before attempting to + // decode it. Sadly, this can only be done by seeking for the start code + // of the next picture. Also, if we didn't find the start code for the + // next picture, but the source has ended, we assume that this last + // picture is in the buffer. + if ( + plm_buffer_has_start_code(self->buffer, PLM_START_PICTURE) == -1 && + !plm_buffer_has_ended(self->buffer) + ) { + return NULL; + } + plm_buffer_discard_read_bytes(self->buffer); + + plm_video_decode_picture(self); + + if (self->assume_no_b_frames) { + frame = &self->frame_backward; + } + else if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_B) { + frame = &self->frame_current; + } + else if (self->has_reference_frame) { + frame = &self->frame_forward; + } + else { + self->has_reference_frame = TRUE; + } + } while (!frame); + + frame->time = self->time; + self->frames_decoded++; + self->time = (double)self->frames_decoded / self->framerate; + + return frame; +} + +int plm_video_has_header(plm_video_t *self) { + if (self->has_sequence_header) { + return TRUE; + } + + if (self->start_code != PLM_START_SEQUENCE) { + self->start_code = plm_buffer_find_start_code(self->buffer, PLM_START_SEQUENCE); + } + if (self->start_code == -1) { + return FALSE; + } + + if (!plm_video_decode_sequence_header(self)) { + return FALSE; + } + + return TRUE; +} + +int plm_video_decode_sequence_header(plm_video_t *self) { + int max_header_size = 64 + 2 * 64 * 8; // 64 bit header + 2x 64 byte matrix + if (!plm_buffer_has(self->buffer, max_header_size)) { + return FALSE; + } + + self->width = plm_buffer_read(self->buffer, 12); + self->height = plm_buffer_read(self->buffer, 12); + + if (self->width <= 0 || self->height <= 0) { + return FALSE; + } + + // Skip pixel aspect ratio + plm_buffer_skip(self->buffer, 4); + + self->framerate = PLM_VIDEO_PICTURE_RATE[plm_buffer_read(self->buffer, 4)]; + + // Skip bit_rate, marker, buffer_size and constrained bit + plm_buffer_skip(self->buffer, 18 + 1 + 10 + 1); + + // Load custom intra quant matrix? + if (plm_buffer_read(self->buffer, 1)) { + for (int i = 0; i < 64; i++) { + int idx = PLM_VIDEO_ZIG_ZAG[i]; + self->intra_quant_matrix[idx] = plm_buffer_read(self->buffer, 8); + } + } + else { + memcpy(self->intra_quant_matrix, PLM_VIDEO_INTRA_QUANT_MATRIX, 64); + } + + // Load custom non intra quant matrix? + if (plm_buffer_read(self->buffer, 1)) { + for (int i = 0; i < 64; i++) { + int idx = PLM_VIDEO_ZIG_ZAG[i]; + self->non_intra_quant_matrix[idx] = plm_buffer_read(self->buffer, 8); + } + } + else { + memcpy(self->non_intra_quant_matrix, PLM_VIDEO_NON_INTRA_QUANT_MATRIX, 64); + } + + self->mb_width = (self->width + 15) >> 4; + self->mb_height = (self->height + 15) >> 4; + self->mb_size = self->mb_width * self->mb_height; + + self->luma_width = self->mb_width << 4; + self->luma_height = self->mb_height << 4; + + self->chroma_width = self->mb_width << 3; + self->chroma_height = self->mb_height << 3; + + + // Allocate one big chunk of data for all 3 frames = 9 planes + size_t luma_plane_size = self->luma_width * self->luma_height; + size_t chroma_plane_size = self->chroma_width * self->chroma_height; + size_t frame_data_size = (luma_plane_size + 2 * chroma_plane_size); + + self->frames_data = (uint8_t*)malloc(frame_data_size * 3); + plm_video_init_frame(self, &self->frame_current, self->frames_data + frame_data_size * 0); + plm_video_init_frame(self, &self->frame_forward, self->frames_data + frame_data_size * 1); + plm_video_init_frame(self, &self->frame_backward, self->frames_data + frame_data_size * 2); + + self->has_sequence_header = TRUE; + return TRUE; +} + +void plm_video_init_frame(plm_video_t *self, plm_frame_t *frame, uint8_t *base) { + size_t luma_plane_size = self->luma_width * self->luma_height; + size_t chroma_plane_size = self->chroma_width * self->chroma_height; + + frame->width = self->width; + frame->height = self->height; + frame->y.width = self->luma_width; + frame->y.height = self->luma_height; + frame->y.data = base; + + frame->cr.width = self->chroma_width; + frame->cr.height = self->chroma_height; + frame->cr.data = base + luma_plane_size; + + frame->cb.width = self->chroma_width; + frame->cb.height = self->chroma_height; + frame->cb.data = base + luma_plane_size + chroma_plane_size; +} + +void plm_video_decode_picture(plm_video_t *self) { + plm_buffer_skip(self->buffer, 10); // skip temporalReference + self->picture_type = plm_buffer_read(self->buffer, 3); + plm_buffer_skip(self->buffer, 16); // skip vbv_delay + + // D frames or unknown coding type + if (self->picture_type <= 0 || self->picture_type > PLM_VIDEO_PICTURE_TYPE_B) { + return; + } + + // Forward full_px, f_code + if ( + self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE || + self->picture_type == PLM_VIDEO_PICTURE_TYPE_B + ) { + self->motion_forward.full_px = plm_buffer_read(self->buffer, 1); + int f_code = plm_buffer_read(self->buffer, 3); + if (f_code == 0) { + // Ignore picture with zero f_code + return; + } + self->motion_forward.r_size = f_code - 1; + } + + // Backward full_px, f_code + if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_B) { + self->motion_backward.full_px = plm_buffer_read(self->buffer, 1); + int f_code = plm_buffer_read(self->buffer, 3); + if (f_code == 0) { + // Ignore picture with zero f_code + return; + } + self->motion_backward.r_size = f_code - 1; + } + + plm_frame_t frame_temp = self->frame_forward; + if ( + self->picture_type == PLM_VIDEO_PICTURE_TYPE_INTRA || + self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE + ) { + self->frame_forward = self->frame_backward; + } + + + // Find first slice start code; skip extension and user data + do { + self->start_code = plm_buffer_next_start_code(self->buffer); + } while ( + self->start_code == PLM_START_EXTENSION || + self->start_code == PLM_START_USER_DATA + ); + + // Decode all slices + while (PLM_START_IS_SLICE(self->start_code)) { + plm_video_decode_slice(self, self->start_code & 0x000000FF); + if (self->macroblock_address >= self->mb_size - 2) { + break; + } + self->start_code = plm_buffer_next_start_code(self->buffer); + } + + // If this is a reference picture rotate the prediction pointers + if ( + self->picture_type == PLM_VIDEO_PICTURE_TYPE_INTRA || + self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE + ) { + self->frame_backward = self->frame_current; + self->frame_current = frame_temp; + } +} + +void plm_video_decode_slice(plm_video_t *self, int slice) { + self->slice_begin = TRUE; + self->macroblock_address = (slice - 1) * self->mb_width - 1; + + // Reset motion vectors and DC predictors + self->motion_backward.h = self->motion_forward.h = 0; + self->motion_backward.v = self->motion_forward.v = 0; + self->dc_predictor[0] = 128; + self->dc_predictor[1] = 128; + self->dc_predictor[2] = 128; + + self->quantizer_scale = plm_buffer_read(self->buffer, 5); + + // Skip extra + while (plm_buffer_read(self->buffer, 1)) { + plm_buffer_skip(self->buffer, 8); + } + + do { + plm_video_decode_macroblock(self); + } while ( + self->macroblock_address < self->mb_size - 1 && + plm_buffer_peek_non_zero(self->buffer, 23) + ); +} + +void plm_video_decode_macroblock(plm_video_t *self) { + // Decode increment + int increment = 0; + int t = plm_buffer_read_vlc(self->buffer, PLM_VIDEO_MACROBLOCK_ADDRESS_INCREMENT); + + while (t == 34) { + // macroblock_stuffing + t = plm_buffer_read_vlc(self->buffer, PLM_VIDEO_MACROBLOCK_ADDRESS_INCREMENT); + } + while (t == 35) { + // macroblock_escape + increment += 33; + t = plm_buffer_read_vlc(self->buffer, PLM_VIDEO_MACROBLOCK_ADDRESS_INCREMENT); + } + increment += t; + + // Process any skipped macroblocks + if (self->slice_begin) { + // The first increment of each slice is relative to beginning of the + // previous row, not the previous macroblock + self->slice_begin = FALSE; + self->macroblock_address += increment; + } + else { + if (self->macroblock_address + increment >= self->mb_size) { + return; // invalid + } + if (increment > 1) { + // Skipped macroblocks reset DC predictors + self->dc_predictor[0] = 128; + self->dc_predictor[1] = 128; + self->dc_predictor[2] = 128; + + // Skipped macroblocks in P-pictures reset motion vectors + if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE) { + self->motion_forward.h = 0; + self->motion_forward.v = 0; + } + } + + // Predict skipped macroblocks + while (increment > 1) { + self->macroblock_address++; + self->mb_row = self->macroblock_address / self->mb_width; + self->mb_col = self->macroblock_address % self->mb_width; + + plm_video_predict_macroblock(self); + increment--; + } + self->macroblock_address++; + } + + self->mb_row = self->macroblock_address / self->mb_width; + self->mb_col = self->macroblock_address % self->mb_width; + + if (self->mb_col >= self->mb_width || self->mb_row >= self->mb_height) { + return; // corrupt stream; + } + + // Process the current macroblock + const plm_vlc_t *table = PLM_VIDEO_MACROBLOCK_TYPE[self->picture_type]; + self->macroblock_type = plm_buffer_read_vlc(self->buffer, table); + + self->macroblock_intra = (self->macroblock_type & 0x01); + self->motion_forward.is_set = (self->macroblock_type & 0x08); + self->motion_backward.is_set = (self->macroblock_type & 0x04); + + // Quantizer scale + if ((self->macroblock_type & 0x10) != 0) { + self->quantizer_scale = plm_buffer_read(self->buffer, 5); + } + + if (self->macroblock_intra) { + // Intra-coded macroblocks reset motion vectors + self->motion_backward.h = self->motion_forward.h = 0; + self->motion_backward.v = self->motion_forward.v = 0; + } + else { + // Non-intra macroblocks reset DC predictors + self->dc_predictor[0] = 128; + self->dc_predictor[1] = 128; + self->dc_predictor[2] = 128; + + plm_video_decode_motion_vectors(self); + plm_video_predict_macroblock(self); + } + + // Decode blocks + int cbp = ((self->macroblock_type & 0x02) != 0) + ? plm_buffer_read_vlc(self->buffer, PLM_VIDEO_CODE_BLOCK_PATTERN) + : (self->macroblock_intra ? 0x3f : 0); + + for (int block = 0, mask = 0x20; block < 6; block++) { + if ((cbp & mask) != 0) { + plm_video_decode_block(self, block); + } + mask >>= 1; + } +} + +void plm_video_decode_motion_vectors(plm_video_t *self) { + + // Forward + if (self->motion_forward.is_set) { + int r_size = self->motion_forward.r_size; + self->motion_forward.h = plm_video_decode_motion_vector(self, r_size, self->motion_forward.h); + self->motion_forward.v = plm_video_decode_motion_vector(self, r_size, self->motion_forward.v); + } + else if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE) { + // No motion information in P-picture, reset vectors + self->motion_forward.h = 0; + self->motion_forward.v = 0; + } + + if (self->motion_backward.is_set) { + int r_size = self->motion_backward.r_size; + self->motion_backward.h = plm_video_decode_motion_vector(self, r_size, self->motion_backward.h); + self->motion_backward.v = plm_video_decode_motion_vector(self, r_size, self->motion_backward.v); + } +} + +int plm_video_decode_motion_vector(plm_video_t *self, int r_size, int motion) { + int fscale = 1 << r_size; + int m_code = plm_buffer_read_vlc(self->buffer, PLM_VIDEO_MOTION); + int r = 0; + int d; + + if ((m_code != 0) && (fscale != 1)) { + r = plm_buffer_read(self->buffer, r_size); + d = ((abs(m_code) - 1) << r_size) + r + 1; + if (m_code < 0) { + d = -d; + } + } + else { + d = m_code; + } + + motion += d; + if (motion > (fscale << 4) - 1) { + motion -= fscale << 5; + } + else if (motion < ((-fscale) << 4)) { + motion += fscale << 5; + } + + return motion; +} + +void plm_video_predict_macroblock(plm_video_t *self) { + int fw_h = self->motion_forward.h; + int fw_v = self->motion_forward.v; + + if (self->motion_forward.full_px) { + fw_h <<= 1; + fw_v <<= 1; + } + + if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_B) { + int bw_h = self->motion_backward.h; + int bw_v = self->motion_backward.v; + + if (self->motion_backward.full_px) { + bw_h <<= 1; + bw_v <<= 1; + } + + if (self->motion_forward.is_set) { + plm_video_copy_macroblock(self, &self->frame_forward, fw_h, fw_v); + if (self->motion_backward.is_set) { + plm_video_interpolate_macroblock(self, &self->frame_backward, bw_h, bw_v); + } + } + else { + plm_video_copy_macroblock(self, &self->frame_backward, bw_h, bw_v); + } + } + else { + plm_video_copy_macroblock(self, &self->frame_forward, fw_h, fw_v); + } +} + +void plm_video_copy_macroblock(plm_video_t *self, plm_frame_t *s, int motion_h, int motion_v) { + plm_frame_t *d = &self->frame_current; + plm_video_process_macroblock(self, s->y.data, d->y.data, motion_h, motion_v, 16, FALSE); + plm_video_process_macroblock(self, s->cr.data, d->cr.data, motion_h / 2, motion_v / 2, 8, FALSE); + plm_video_process_macroblock(self, s->cb.data, d->cb.data, motion_h / 2, motion_v / 2, 8, FALSE); +} + +void plm_video_interpolate_macroblock(plm_video_t *self, plm_frame_t *s, int motion_h, int motion_v) { + plm_frame_t *d = &self->frame_current; + plm_video_process_macroblock(self, s->y.data, d->y.data, motion_h, motion_v, 16, TRUE); + plm_video_process_macroblock(self, s->cr.data, d->cr.data, motion_h / 2, motion_v / 2, 8, TRUE); + plm_video_process_macroblock(self, s->cb.data, d->cb.data, motion_h / 2, motion_v / 2, 8, TRUE); +} + +#define PLM_BLOCK_SET(DEST, DEST_INDEX, DEST_WIDTH, SOURCE_INDEX, SOURCE_WIDTH, BLOCK_SIZE, OP) do { \ + int dest_scan = DEST_WIDTH - BLOCK_SIZE; \ + int source_scan = SOURCE_WIDTH - BLOCK_SIZE; \ + for (int y = 0; y < BLOCK_SIZE; y++) { \ + for (int x = 0; x < BLOCK_SIZE; x++) { \ + DEST[DEST_INDEX] = OP; \ + SOURCE_INDEX++; DEST_INDEX++; \ + } \ + SOURCE_INDEX += source_scan; \ + DEST_INDEX += dest_scan; \ + }} while(FALSE) + +void plm_video_process_macroblock( + plm_video_t *self, uint8_t *s, uint8_t *d, + int motion_h, int motion_v, int block_size, int interpolate +) { + int dw = self->mb_width * block_size; + + int hp = motion_h >> 1; + int vp = motion_v >> 1; + int odd_h = (motion_h & 1) == 1; + int odd_v = (motion_v & 1) == 1; + + unsigned int si = ((self->mb_row * block_size) + vp) * dw + (self->mb_col * block_size) + hp; + unsigned int di = (self->mb_row * dw + self->mb_col) * block_size; + + unsigned int max_address = (dw * (self->mb_height * block_size - block_size + 1) - block_size); + if (si > max_address || di > max_address) { + return; // corrupt video + } + + #define PLM_MB_CASE(INTERPOLATE, ODD_H, ODD_V, OP) \ + case ((INTERPOLATE << 2) | (ODD_H << 1) | (ODD_V)): \ + PLM_BLOCK_SET(d, di, dw, si, dw, block_size, OP); \ + break + + switch ((interpolate << 2) | (odd_h << 1) | (odd_v)) { + PLM_MB_CASE(0, 0, 0, (s[si])); + PLM_MB_CASE(0, 0, 1, (s[si] + s[si + dw] + 1) >> 1); + PLM_MB_CASE(0, 1, 0, (s[si] + s[si + 1] + 1) >> 1); + PLM_MB_CASE(0, 1, 1, (s[si] + s[si + 1] + s[si + dw] + s[si + dw + 1] + 2) >> 2); + + PLM_MB_CASE(1, 0, 0, (d[di] + (s[si]) + 1) >> 1); + PLM_MB_CASE(1, 0, 1, (d[di] + ((s[si] + s[si + dw] + 1) >> 1) + 1) >> 1); + PLM_MB_CASE(1, 1, 0, (d[di] + ((s[si] + s[si + 1] + 1) >> 1) + 1) >> 1); + PLM_MB_CASE(1, 1, 1, (d[di] + ((s[si] + s[si + 1] + s[si + dw] + s[si + dw + 1] + 2) >> 2) + 1) >> 1); + } + + #undef PLM_MB_CASE +} + +void plm_video_decode_block(plm_video_t *self, int block) { + + int n = 0; + uint8_t *quant_matrix; + + // Decode DC coefficient of intra-coded blocks + if (self->macroblock_intra) { + int predictor; + int dct_size; + + // DC prediction + int plane_index = block > 3 ? block - 3 : 0; + predictor = self->dc_predictor[plane_index]; + dct_size = plm_buffer_read_vlc(self->buffer, PLM_VIDEO_DCT_SIZE[plane_index]); + + // Read DC coeff + if (dct_size > 0) { + int differential = plm_buffer_read(self->buffer, dct_size); + if ((differential & (1 << (dct_size - 1))) != 0) { + self->block_data[0] = predictor + differential; + } + else { + self->block_data[0] = predictor + (-(1 << dct_size) | (differential + 1)); + } + } + else { + self->block_data[0] = predictor; + } + + // Save predictor value + self->dc_predictor[plane_index] = self->block_data[0]; + + // Dequantize + premultiply + self->block_data[0] <<= (3 + 5); + + quant_matrix = self->intra_quant_matrix; + n = 1; + } + else { + quant_matrix = self->non_intra_quant_matrix; + } + + // Decode AC coefficients (+DC for non-intra) + int level = 0; + while (TRUE) { + int run = 0; + uint16_t coeff = plm_buffer_read_vlc_uint(self->buffer, PLM_VIDEO_DCT_COEFF); + + if ((coeff == 0x0001) && (n > 0) && (plm_buffer_read(self->buffer, 1) == 0)) { + // end_of_block + break; + } + if (coeff == 0xffff) { + // escape + run = plm_buffer_read(self->buffer, 6); + level = plm_buffer_read(self->buffer, 8); + if (level == 0) { + level = plm_buffer_read(self->buffer, 8); + } + else if (level == 128) { + level = plm_buffer_read(self->buffer, 8) - 256; + } + else if (level > 128) { + level = level - 256; + } + } + else { + run = coeff >> 8; + level = coeff & 0xff; + if (plm_buffer_read(self->buffer, 1)) { + level = -level; + } + } + + n += run; + if (n < 0 || n >= 64) { + return; // invalid + } + + int de_zig_zagged = PLM_VIDEO_ZIG_ZAG[n]; + n++; + + // Dequantize, oddify, clip + level <<= 1; + if (!self->macroblock_intra) { + level += (level < 0 ? -1 : 1); + } + level = (level * self->quantizer_scale * quant_matrix[de_zig_zagged]) >> 4; + if ((level & 1) == 0) { + level -= level > 0 ? 1 : -1; + } + if (level > 2047) { + level = 2047; + } + else if (level < -2048) { + level = -2048; + } + + // Save premultiplied coefficient + self->block_data[de_zig_zagged] = level * PLM_VIDEO_PREMULTIPLIER_MATRIX[de_zig_zagged]; + } + + // Move block to its place + uint8_t *d; + int dw; + int di; + + if (block < 4) { + d = self->frame_current.y.data; + dw = self->luma_width; + di = (self->mb_row * self->luma_width + self->mb_col) << 4; + if ((block & 1) != 0) { + di += 8; + } + if ((block & 2) != 0) { + di += self->luma_width << 3; + } + } + else { + d = (block == 4) ? self->frame_current.cb.data : self->frame_current.cr.data; + dw = self->chroma_width; + di = ((self->mb_row * self->luma_width) << 2) + (self->mb_col << 3); + } + + int *s = self->block_data; + int si = 0; + if (self->macroblock_intra) { + // Overwrite (no prediction) + if (n == 1) { + int clamped = plm_clamp((s[0] + 128) >> 8); + PLM_BLOCK_SET(d, di, dw, si, 8, 8, clamped); + s[0] = 0; + } + else { + plm_video_idct(s); + PLM_BLOCK_SET(d, di, dw, si, 8, 8, plm_clamp(s[si])); + memset(self->block_data, 0, sizeof(self->block_data)); + } + } + else { + // Add data to the predicted macroblock + if (n == 1) { + int value = (s[0] + 128) >> 8; + PLM_BLOCK_SET(d, di, dw, si, 8, 8, plm_clamp(d[di] + value)); + s[0] = 0; + } + else { + plm_video_idct(s); + PLM_BLOCK_SET(d, di, dw, si, 8, 8, plm_clamp(d[di] + s[si])); + memset(self->block_data, 0, sizeof(self->block_data)); + } + } +} + +void plm_video_idct(int *block) { + int + b1, b3, b4, b6, b7, tmp1, tmp2, m0, + x0, x1, x2, x3, x4, y3, y4, y5, y6, y7; + + // Transform columns + for (int i = 0; i < 8; ++i) { + b1 = block[4 * 8 + i]; + b3 = block[2 * 8 + i] + block[6 * 8 + i]; + b4 = block[5 * 8 + i] - block[3 * 8 + i]; + tmp1 = block[1 * 8 + i] + block[7 * 8 + i]; + tmp2 = block[3 * 8 + i] + block[5 * 8 + i]; + b6 = block[1 * 8 + i] - block[7 * 8 + i]; + b7 = tmp1 + tmp2; + m0 = block[0 * 8 + i]; + x4 = ((b6 * 473 - b4 * 196 + 128) >> 8) - b7; + x0 = x4 - (((tmp1 - tmp2) * 362 + 128) >> 8); + x1 = m0 - b1; + x2 = (((block[2 * 8 + i] - block[6 * 8 + i]) * 362 + 128) >> 8) - b3; + x3 = m0 + b1; + y3 = x1 + x2; + y4 = x3 + b3; + y5 = x1 - x2; + y6 = x3 - b3; + y7 = -x0 - ((b4 * 473 + b6 * 196 + 128) >> 8); + block[0 * 8 + i] = b7 + y4; + block[1 * 8 + i] = x4 + y3; + block[2 * 8 + i] = y5 - x0; + block[3 * 8 + i] = y6 - y7; + block[4 * 8 + i] = y6 + y7; + block[5 * 8 + i] = x0 + y5; + block[6 * 8 + i] = y3 - x4; + block[7 * 8 + i] = y4 - b7; + } + + // Transform rows + for (int i = 0; i < 64; i += 8) { + b1 = block[4 + i]; + b3 = block[2 + i] + block[6 + i]; + b4 = block[5 + i] - block[3 + i]; + tmp1 = block[1 + i] + block[7 + i]; + tmp2 = block[3 + i] + block[5 + i]; + b6 = block[1 + i] - block[7 + i]; + b7 = tmp1 + tmp2; + m0 = block[0 + i]; + x4 = ((b6 * 473 - b4 * 196 + 128) >> 8) - b7; + x0 = x4 - (((tmp1 - tmp2) * 362 + 128) >> 8); + x1 = m0 - b1; + x2 = (((block[2 + i] - block[6 + i]) * 362 + 128) >> 8) - b3; + x3 = m0 + b1; + y3 = x1 + x2; + y4 = x3 + b3; + y5 = x1 - x2; + y6 = x3 - b3; + y7 = -x0 - ((b4 * 473 + b6 * 196 + 128) >> 8); + block[0 + i] = (b7 + y4 + 128) >> 8; + block[1 + i] = (x4 + y3 + 128) >> 8; + block[2 + i] = (y5 - x0 + 128) >> 8; + block[3 + i] = (y6 - y7 + 128) >> 8; + block[4 + i] = (y6 + y7 + 128) >> 8; + block[5 + i] = (x0 + y5 + 128) >> 8; + block[6 + i] = (y3 - x4 + 128) >> 8; + block[7 + i] = (y4 - b7 + 128) >> 8; + } +} + +// YCbCr conversion following the BT.601 standard: +// https://infogalactic.com/info/YCbCr#ITU-R_BT.601_conversion + +#define PLM_PUT_PIXEL(RI, GI, BI, Y_OFFSET, DEST_OFFSET) \ + y = ((frame->y.data[y_index + Y_OFFSET]-16) * 76309) >> 16; \ + dest[d_index + DEST_OFFSET + RI] = plm_clamp(y + r); \ + dest[d_index + DEST_OFFSET + GI] = plm_clamp(y - g); \ + dest[d_index + DEST_OFFSET + BI] = plm_clamp(y + b); + +#define PLM_DEFINE_FRAME_CONVERT_FUNCTION(NAME, BYTES_PER_PIXEL, RI, GI, BI) \ + void NAME(plm_frame_t *frame, uint8_t *dest, int stride) { \ + int cols = frame->width >> 1; \ + int rows = frame->height >> 1; \ + int yw = frame->y.width; \ + int cw = frame->cb.width; \ + for (int row = 0; row < rows; row++) { \ + int c_index = row * cw; \ + int y_index = row * 2 * yw; \ + int d_index = row * 2 * stride; \ + for (int col = 0; col < cols; col++) { \ + int y; \ + int cr = frame->cr.data[c_index] - 128; \ + int cb = frame->cb.data[c_index] - 128; \ + int r = (cr * 104597) >> 16; \ + int g = (cb * 25674 + cr * 53278) >> 16; \ + int b = (cb * 132201) >> 16; \ + PLM_PUT_PIXEL(RI, GI, BI, 0, 0); \ + PLM_PUT_PIXEL(RI, GI, BI, 1, BYTES_PER_PIXEL); \ + PLM_PUT_PIXEL(RI, GI, BI, yw, stride); \ + PLM_PUT_PIXEL(RI, GI, BI, yw + 1, stride + BYTES_PER_PIXEL); \ + c_index += 1; \ + y_index += 2; \ + d_index += 2 * BYTES_PER_PIXEL; \ + } \ + } \ + } + +PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_rgb, 3, 0, 1, 2) +PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_bgr, 3, 2, 1, 0) +PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_rgba, 4, 0, 1, 2) +PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_bgra, 4, 2, 1, 0) +PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_argb, 4, 1, 2, 3) +PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_abgr, 4, 3, 2, 1) + + +#undef PLM_PUT_PIXEL +#undef PLM_DEFINE_FRAME_CONVERT_FUNCTION + + + +// ----------------------------------------------------------------------------- +// plm_audio implementation + +// Based on kjmp2 by Martin J. Fiedler +// http://keyj.emphy.de/kjmp2/ + +static const int PLM_AUDIO_FRAME_SYNC = 0x7ff; + +static const int PLM_AUDIO_MPEG_2_5 = 0x0; +static const int PLM_AUDIO_MPEG_2 = 0x2; +static const int PLM_AUDIO_MPEG_1 = 0x3; + +static const int PLM_AUDIO_LAYER_III = 0x1; +static const int PLM_AUDIO_LAYER_II = 0x2; +static const int PLM_AUDIO_LAYER_I = 0x3; + +static const int PLM_AUDIO_MODE_STEREO = 0x0; +static const int PLM_AUDIO_MODE_JOINT_STEREO = 0x1; +static const int PLM_AUDIO_MODE_DUAL_CHANNEL = 0x2; +static const int PLM_AUDIO_MODE_MONO = 0x3; + +static const unsigned short PLM_AUDIO_SAMPLE_RATE[] = { + 44100, 48000, 32000, 0, // MPEG-1 + 22050, 24000, 16000, 0 // MPEG-2 +}; + +static const short PLM_AUDIO_BIT_RATE[] = { + 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, // MPEG-1 + 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160 // MPEG-2 +}; + +static const int PLM_AUDIO_SCALEFACTOR_BASE[] = { + 0x02000000, 0x01965FEA, 0x01428A30 +}; + +static const float PLM_AUDIO_SYNTHESIS_WINDOW[] = { + 0.0, -0.5, -0.5, -0.5, -0.5, -0.5, + -0.5, -1.0, -1.0, -1.0, -1.0, -1.5, + -1.5, -2.0, -2.0, -2.5, -2.5, -3.0, + -3.5, -3.5, -4.0, -4.5, -5.0, -5.5, + -6.5, -7.0, -8.0, -8.5, -9.5, -10.5, + -12.0, -13.0, -14.5, -15.5, -17.5, -19.0, + -20.5, -22.5, -24.5, -26.5, -29.0, -31.5, + -34.0, -36.5, -39.5, -42.5, -45.5, -48.5, + -52.0, -55.5, -58.5, -62.5, -66.0, -69.5, + -73.5, -77.0, -80.5, -84.5, -88.0, -91.5, + -95.0, -98.0, -101.0, -104.0, 106.5, 109.0, + 111.0, 112.5, 113.5, 114.0, 114.0, 113.5, + 112.0, 110.5, 107.5, 104.0, 100.0, 94.5, + 88.5, 81.5, 73.0, 63.5, 53.0, 41.5, + 28.5, 14.5, -1.0, -18.0, -36.0, -55.5, + -76.5, -98.5, -122.0, -147.0, -173.5, -200.5, + -229.5, -259.5, -290.5, -322.5, -355.5, -389.5, + -424.0, -459.5, -495.5, -532.0, -568.5, -605.0, + -641.5, -678.0, -714.0, -749.0, -783.5, -817.0, + -849.0, -879.5, -908.5, -935.0, -959.5, -981.0, + -1000.5, -1016.0, -1028.5, -1037.5, -1042.5, -1043.5, + -1040.0, -1031.5, 1018.5, 1000.0, 976.0, 946.5, + 911.0, 869.5, 822.0, 767.5, 707.0, 640.0, + 565.5, 485.0, 397.0, 302.5, 201.0, 92.5, + -22.5, -144.0, -272.5, -407.0, -547.5, -694.0, + -846.0, -1003.0, -1165.0, -1331.5, -1502.0, -1675.5, + -1852.5, -2031.5, -2212.5, -2394.0, -2576.5, -2758.5, + -2939.5, -3118.5, -3294.5, -3467.5, -3635.5, -3798.5, + -3955.0, -4104.5, -4245.5, -4377.5, -4499.0, -4609.5, + -4708.0, -4792.5, -4863.5, -4919.0, -4958.0, -4979.5, + -4983.0, -4967.5, -4931.5, -4875.0, -4796.0, -4694.5, + -4569.5, -4420.0, -4246.0, -4046.0, -3820.0, -3567.0, + 3287.0, 2979.5, 2644.0, 2280.5, 1888.0, 1467.5, + 1018.5, 541.0, 35.0, -499.0, -1061.0, -1650.0, + -2266.5, -2909.0, -3577.0, -4270.0, -4987.5, -5727.5, + -6490.0, -7274.0, -8077.5, -8899.5, -9739.0, -10594.5, + -11464.5, -12347.0, -13241.0, -14144.5, -15056.0, -15973.5, + -16895.5, -17820.0, -18744.5, -19668.0, -20588.0, -21503.0, + -22410.5, -23308.5, -24195.0, -25068.5, -25926.5, -26767.0, + -27589.0, -28389.0, -29166.5, -29919.0, -30644.5, -31342.0, + -32009.5, -32645.0, -33247.0, -33814.5, -34346.0, -34839.5, + -35295.0, -35710.0, -36084.5, -36417.5, -36707.5, -36954.0, + -37156.5, -37315.0, -37428.0, -37496.0, 37519.0, 37496.0, + 37428.0, 37315.0, 37156.5, 36954.0, 36707.5, 36417.5, + 36084.5, 35710.0, 35295.0, 34839.5, 34346.0, 33814.5, + 33247.0, 32645.0, 32009.5, 31342.0, 30644.5, 29919.0, + 29166.5, 28389.0, 27589.0, 26767.0, 25926.5, 25068.5, + 24195.0, 23308.5, 22410.5, 21503.0, 20588.0, 19668.0, + 18744.5, 17820.0, 16895.5, 15973.5, 15056.0, 14144.5, + 13241.0, 12347.0, 11464.5, 10594.5, 9739.0, 8899.5, + 8077.5, 7274.0, 6490.0, 5727.5, 4987.5, 4270.0, + 3577.0, 2909.0, 2266.5, 1650.0, 1061.0, 499.0, + -35.0, -541.0, -1018.5, -1467.5, -1888.0, -2280.5, + -2644.0, -2979.5, 3287.0, 3567.0, 3820.0, 4046.0, + 4246.0, 4420.0, 4569.5, 4694.5, 4796.0, 4875.0, + 4931.5, 4967.5, 4983.0, 4979.5, 4958.0, 4919.0, + 4863.5, 4792.5, 4708.0, 4609.5, 4499.0, 4377.5, + 4245.5, 4104.5, 3955.0, 3798.5, 3635.5, 3467.5, + 3294.5, 3118.5, 2939.5, 2758.5, 2576.5, 2394.0, + 2212.5, 2031.5, 1852.5, 1675.5, 1502.0, 1331.5, + 1165.0, 1003.0, 846.0, 694.0, 547.5, 407.0, + 272.5, 144.0, 22.5, -92.5, -201.0, -302.5, + -397.0, -485.0, -565.5, -640.0, -707.0, -767.5, + -822.0, -869.5, -911.0, -946.5, -976.0, -1000.0, + 1018.5, 1031.5, 1040.0, 1043.5, 1042.5, 1037.5, + 1028.5, 1016.0, 1000.5, 981.0, 959.5, 935.0, + 908.5, 879.5, 849.0, 817.0, 783.5, 749.0, + 714.0, 678.0, 641.5, 605.0, 568.5, 532.0, + 495.5, 459.5, 424.0, 389.5, 355.5, 322.5, + 290.5, 259.5, 229.5, 200.5, 173.5, 147.0, + 122.0, 98.5, 76.5, 55.5, 36.0, 18.0, + 1.0, -14.5, -28.5, -41.5, -53.0, -63.5, + -73.0, -81.5, -88.5, -94.5, -100.0, -104.0, + -107.5, -110.5, -112.0, -113.5, -114.0, -114.0, + -113.5, -112.5, -111.0, -109.0, 106.5, 104.0, + 101.0, 98.0, 95.0, 91.5, 88.0, 84.5, + 80.5, 77.0, 73.5, 69.5, 66.0, 62.5, + 58.5, 55.5, 52.0, 48.5, 45.5, 42.5, + 39.5, 36.5, 34.0, 31.5, 29.0, 26.5, + 24.5, 22.5, 20.5, 19.0, 17.5, 15.5, + 14.5, 13.0, 12.0, 10.5, 9.5, 8.5, + 8.0, 7.0, 6.5, 5.5, 5.0, 4.5, + 4.0, 3.5, 3.5, 3.0, 2.5, 2.5, + 2.0, 2.0, 1.5, 1.5, 1.0, 1.0, + 1.0, 1.0, 0.5, 0.5, 0.5, 0.5, + 0.5, 0.5 +}; + +// Quantizer lookup, step 1: bitrate classes +static const uint8_t PLM_AUDIO_QUANT_LUT_STEP_1[2][16] = { + // 32, 48, 56, 64, 80, 96,112,128,160,192,224,256,320,384 <- bitrate + { 0, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2 }, // mono + // 16, 24, 28, 32, 40, 48, 56, 64, 80, 96,112,128,160,192 <- bitrate / chan + { 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2 } // stereo +}; + +// Quantizer lookup, step 2: bitrate class, sample rate -> B2 table idx, sblimit +#define PLM_AUDIO_QUANT_TAB_A (27 | 64) // Table 3-B.2a: high-rate, sblimit = 27 +#define PLM_AUDIO_QUANT_TAB_B (30 | 64) // Table 3-B.2b: high-rate, sblimit = 30 +#define PLM_AUDIO_QUANT_TAB_C 8 // Table 3-B.2c: low-rate, sblimit = 8 +#define PLM_AUDIO_QUANT_TAB_D 12 // Table 3-B.2d: low-rate, sblimit = 12 + +static const uint8_t QUANT_LUT_STEP_2[3][3] = { + //44.1 kHz, 48 kHz, 32 kHz + { PLM_AUDIO_QUANT_TAB_C, PLM_AUDIO_QUANT_TAB_C, PLM_AUDIO_QUANT_TAB_D }, // 32 - 48 kbit/sec/ch + { PLM_AUDIO_QUANT_TAB_A, PLM_AUDIO_QUANT_TAB_A, PLM_AUDIO_QUANT_TAB_A }, // 56 - 80 kbit/sec/ch + { PLM_AUDIO_QUANT_TAB_B, PLM_AUDIO_QUANT_TAB_A, PLM_AUDIO_QUANT_TAB_B } // 96+ kbit/sec/ch +}; + +// Quantizer lookup, step 3: B2 table, subband -> nbal, row index +// (upper 4 bits: nbal, lower 4 bits: row index) +static const uint8_t PLM_AUDIO_QUANT_LUT_STEP_3[3][32] = { + // Low-rate table (3-B.2c and 3-B.2d) + { + 0x44,0x44, + 0x34,0x34,0x34,0x34,0x34,0x34,0x34,0x34,0x34,0x34 + }, + // High-rate table (3-B.2a and 3-B.2b) + { + 0x43,0x43,0x43, + 0x42,0x42,0x42,0x42,0x42,0x42,0x42,0x42, + 0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31, + 0x20,0x20,0x20,0x20,0x20,0x20,0x20 + }, + // MPEG-2 LSR table (B.2 in ISO 13818-3) + { + 0x45,0x45,0x45,0x45, + 0x34,0x34,0x34,0x34,0x34,0x34,0x34, + 0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24, + 0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24 + } +}; + +// Quantizer lookup, step 4: table row, allocation[] value -> quant table index +static const uint8_t PLM_AUDIO_QUANT_LUT_STEP_4[6][16] = { + { 0, 1, 2, 17 }, + { 0, 1, 2, 3, 4, 5, 6, 17 }, + { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 17 }, + { 0, 1, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 }, + { 0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17 }, + { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 } +}; + +typedef struct plm_quantizer_spec_t { + unsigned short levels; + unsigned char group; + unsigned char bits; +} plm_quantizer_spec_t; + +static const plm_quantizer_spec_t PLM_AUDIO_QUANT_TAB[] = { + { 3, 1, 5 }, // 1 + { 5, 1, 7 }, // 2 + { 7, 0, 3 }, // 3 + { 9, 1, 10 }, // 4 + { 15, 0, 4 }, // 5 + { 31, 0, 5 }, // 6 + { 63, 0, 6 }, // 7 + { 127, 0, 7 }, // 8 + { 255, 0, 8 }, // 9 + { 511, 0, 9 }, // 10 + { 1023, 0, 10 }, // 11 + { 2047, 0, 11 }, // 12 + { 4095, 0, 12 }, // 13 + { 8191, 0, 13 }, // 14 + { 16383, 0, 14 }, // 15 + { 32767, 0, 15 }, // 16 + { 65535, 0, 16 } // 17 +}; + +struct plm_audio_t { + double time; + int samples_decoded; + int samplerate_index; + int bitrate_index; + int version; + int layer; + int mode; + int bound; + int v_pos; + int next_frame_data_size; + int has_header; + + plm_buffer_t *buffer; + int destroy_buffer_when_done; + + const plm_quantizer_spec_t *allocation[2][32]; + uint8_t scale_factor_info[2][32]; + int scale_factor[2][32][3]; + int sample[2][32][3]; + + plm_samples_t samples; + float D[1024]; + float V[2][1024]; + float U[32]; +}; + +int plm_audio_find_frame_sync(plm_audio_t *self); +int plm_audio_decode_header(plm_audio_t *self); +void plm_audio_decode_frame(plm_audio_t *self); +const plm_quantizer_spec_t *plm_audio_read_allocation(plm_audio_t *self, int sb, int tab3); +void plm_audio_read_samples(plm_audio_t *self, int ch, int sb, int part); +void plm_audio_idct36(int s[32][3], int ss, float *d, int dp); + +plm_audio_t *plm_audio_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done) { + plm_audio_t *self = (plm_audio_t *)malloc(sizeof(plm_audio_t)); + memset(self, 0, sizeof(plm_audio_t)); + + self->samples.count = PLM_AUDIO_SAMPLES_PER_FRAME; + self->buffer = buffer; + self->destroy_buffer_when_done = destroy_when_done; + self->samplerate_index = 3; // Indicates 0 + + memcpy(self->D, PLM_AUDIO_SYNTHESIS_WINDOW, 512 * sizeof(float)); + memcpy(self->D + 512, PLM_AUDIO_SYNTHESIS_WINDOW, 512 * sizeof(float)); + + // Attempt to decode first header + self->next_frame_data_size = plm_audio_decode_header(self); + + return self; +} + +void plm_audio_destroy(plm_audio_t *self) { + if (self->destroy_buffer_when_done) { + plm_buffer_destroy(self->buffer); + } + free(self); +} + +int plm_audio_has_header(plm_audio_t *self) { + if (self->has_header) { + return TRUE; + } + + self->next_frame_data_size = plm_audio_decode_header(self); + return self->has_header; +} + +int plm_audio_get_samplerate(plm_audio_t *self) { + return plm_audio_has_header(self) + ? PLM_AUDIO_SAMPLE_RATE[self->samplerate_index] + : 0; +} + +double plm_audio_get_time(plm_audio_t *self) { + return self->time; +} + +void plm_audio_set_time(plm_audio_t *self, double time) { + self->samples_decoded = time * + (double)PLM_AUDIO_SAMPLE_RATE[self->samplerate_index]; + self->time = time; +} + +void plm_audio_rewind(plm_audio_t *self) { + plm_buffer_rewind(self->buffer); + self->time = 0; + self->samples_decoded = 0; + self->next_frame_data_size = 0; +} + +int plm_audio_has_ended(plm_audio_t *self) { + return plm_buffer_has_ended(self->buffer); +} + +plm_samples_t *plm_audio_decode(plm_audio_t *self) { + // Do we have at least enough information to decode the frame header? + if (!self->next_frame_data_size) { + if (!plm_buffer_has(self->buffer, 48)) { + return NULL; + } + self->next_frame_data_size = plm_audio_decode_header(self); + } + + if ( + self->next_frame_data_size == 0 || + !plm_buffer_has(self->buffer, self->next_frame_data_size << 3) + ) { + return NULL; + } + + plm_audio_decode_frame(self); + self->next_frame_data_size = 0; + + self->samples.time = self->time; + + self->samples_decoded += PLM_AUDIO_SAMPLES_PER_FRAME; + self->time = (double)self->samples_decoded / + (double)PLM_AUDIO_SAMPLE_RATE[self->samplerate_index]; + + return &self->samples; +} + +int plm_audio_find_frame_sync(plm_audio_t *self) { + size_t i; + for (i = self->buffer->bit_index >> 3; i < self->buffer->length-1; i++) { + if ( + self->buffer->bytes[i] == 0xFF && + (self->buffer->bytes[i+1] & 0xFE) == 0xFC + ) { + self->buffer->bit_index = ((i+1) << 3) + 3; + return TRUE; + } + } + self->buffer->bit_index = (i + 1) << 3; + return FALSE; +} + +int plm_audio_decode_header(plm_audio_t *self) { + if (!plm_buffer_has(self->buffer, 48)) { + return 0; + } + + plm_buffer_skip_bytes(self->buffer, 0x00); + int sync = plm_buffer_read(self->buffer, 11); + + + // Attempt to resync if no syncword was found. This sucks balls. The MP2 + // stream contains a syncword just before every frame (11 bits set to 1). + // However, this syncword is not guaranteed to not occur elsewhere in the + // stream. So, if we have to resync, we also have to check if the header + // (samplerate, bitrate) differs from the one we had before. This all + // may still lead to garbage data being decoded :/ + + if (sync != PLM_AUDIO_FRAME_SYNC && !plm_audio_find_frame_sync(self)) { + return 0; + } + + self->version = plm_buffer_read(self->buffer, 2); + self->layer = plm_buffer_read(self->buffer, 2); + int hasCRC = !plm_buffer_read(self->buffer, 1); + + if ( + self->version != PLM_AUDIO_MPEG_1 || + self->layer != PLM_AUDIO_LAYER_II + ) { + return 0; + } + + int bitrate_index = plm_buffer_read(self->buffer, 4) - 1; + if (bitrate_index > 13) { + return 0; + } + + int samplerate_index = plm_buffer_read(self->buffer, 2); + if (samplerate_index == 3) { + return 0; + } + + int padding = plm_buffer_read(self->buffer, 1); + plm_buffer_skip(self->buffer, 1); // f_private + int mode = plm_buffer_read(self->buffer, 2); + + // If we already have a header, make sure the samplerate, bitrate and mode + // are still the same, otherwise we might have missed sync. + if ( + self->has_header && ( + self->bitrate_index != bitrate_index || + self->samplerate_index != samplerate_index || + self->mode != mode + ) + ) { + return 0; + } + + self->bitrate_index = bitrate_index; + self->samplerate_index = samplerate_index; + self->mode = mode; + self->has_header = TRUE; + + // Parse the mode_extension, set up the stereo bound + if (mode == PLM_AUDIO_MODE_JOINT_STEREO) { + self->bound = (plm_buffer_read(self->buffer, 2) + 1) << 2; + } + else { + plm_buffer_skip(self->buffer, 2); + self->bound = (mode == PLM_AUDIO_MODE_MONO) ? 0 : 32; + } + + // Discard the last 4 bits of the header and the CRC value, if present + plm_buffer_skip(self->buffer, 4); // copyright(1), original(1), emphasis(2) + if (hasCRC) { + plm_buffer_skip(self->buffer, 16); + } + + // Compute frame size, check if we have enough data to decode the whole + // frame. + int bitrate = PLM_AUDIO_BIT_RATE[self->bitrate_index]; + int samplerate = PLM_AUDIO_SAMPLE_RATE[self->samplerate_index]; + int frame_size = (144000 * bitrate / samplerate) + padding; + return frame_size - (hasCRC ? 6 : 4); +} + +void plm_audio_decode_frame(plm_audio_t *self) { + // Prepare the quantizer table lookups + int tab3 = 0; + int sblimit = 0; + + int tab1 = (self->mode == PLM_AUDIO_MODE_MONO) ? 0 : 1; + int tab2 = PLM_AUDIO_QUANT_LUT_STEP_1[tab1][self->bitrate_index]; + tab3 = QUANT_LUT_STEP_2[tab2][self->samplerate_index]; + sblimit = tab3 & 63; + tab3 >>= 6; + + if (self->bound > sblimit) { + self->bound = sblimit; + } + + // Read the allocation information + for (int sb = 0; sb < self->bound; sb++) { + self->allocation[0][sb] = plm_audio_read_allocation(self, sb, tab3); + self->allocation[1][sb] = plm_audio_read_allocation(self, sb, tab3); + } + + for (int sb = self->bound; sb < sblimit; sb++) { + self->allocation[0][sb] = + self->allocation[1][sb] = + plm_audio_read_allocation(self, sb, tab3); + } + + // Read scale factor selector information + int channels = (self->mode == PLM_AUDIO_MODE_MONO) ? 1 : 2; + for (int sb = 0; sb < sblimit; sb++) { + for (int ch = 0; ch < channels; ch++) { + if (self->allocation[ch][sb]) { + self->scale_factor_info[ch][sb] = plm_buffer_read(self->buffer, 2); + } + } + if (self->mode == PLM_AUDIO_MODE_MONO) { + self->scale_factor_info[1][sb] = self->scale_factor_info[0][sb]; + } + } + + // Read scale factors + for (int sb = 0; sb < sblimit; sb++) { + for (int ch = 0; ch < channels; ch++) { + if (self->allocation[ch][sb]) { + int *sf = self->scale_factor[ch][sb]; + switch (self->scale_factor_info[ch][sb]) { + case 0: + sf[0] = plm_buffer_read(self->buffer, 6); + sf[1] = plm_buffer_read(self->buffer, 6); + sf[2] = plm_buffer_read(self->buffer, 6); + break; + case 1: + sf[0] = + sf[1] = plm_buffer_read(self->buffer, 6); + sf[2] = plm_buffer_read(self->buffer, 6); + break; + case 2: + sf[0] = + sf[1] = + sf[2] = plm_buffer_read(self->buffer, 6); + break; + case 3: + sf[0] = plm_buffer_read(self->buffer, 6); + sf[1] = + sf[2] = plm_buffer_read(self->buffer, 6); + break; + } + } + } + if (self->mode == PLM_AUDIO_MODE_MONO) { + self->scale_factor[1][sb][0] = self->scale_factor[0][sb][0]; + self->scale_factor[1][sb][1] = self->scale_factor[0][sb][1]; + self->scale_factor[1][sb][2] = self->scale_factor[0][sb][2]; + } + } + + // Coefficient input and reconstruction + int out_pos = 0; + for (int part = 0; part < 3; part++) { + for (int granule = 0; granule < 4; granule++) { + + // Read the samples + for (int sb = 0; sb < self->bound; sb++) { + plm_audio_read_samples(self, 0, sb, part); + plm_audio_read_samples(self, 1, sb, part); + } + for (int sb = self->bound; sb < sblimit; sb++) { + plm_audio_read_samples(self, 0, sb, part); + self->sample[1][sb][0] = self->sample[0][sb][0]; + self->sample[1][sb][1] = self->sample[0][sb][1]; + self->sample[1][sb][2] = self->sample[0][sb][2]; + } + for (int sb = sblimit; sb < 32; sb++) { + self->sample[0][sb][0] = 0; + self->sample[0][sb][1] = 0; + self->sample[0][sb][2] = 0; + self->sample[1][sb][0] = 0; + self->sample[1][sb][1] = 0; + self->sample[1][sb][2] = 0; + } + + // Synthesis loop + for (int p = 0; p < 3; p++) { + // Shifting step + self->v_pos = (self->v_pos - 64) & 1023; + + for (int ch = 0; ch < 2; ch++) { + plm_audio_idct36(self->sample[ch], p, self->V[ch], self->v_pos); + + // Build U, windowing, calculate output + memset(self->U, 0, sizeof(self->U)); + + int d_index = 512 - (self->v_pos >> 1); + int v_index = (self->v_pos % 128) >> 1; + while (v_index < 1024) { + for (int i = 0; i < 32; ++i) { + self->U[i] += self->D[d_index++] * self->V[ch][v_index++]; + } + + v_index += 128 - 32; + d_index += 64 - 32; + } + + d_index -= (512 - 32); + v_index = (128 - 32 + 1024) - v_index; + while (v_index < 1024) { + for (int i = 0; i < 32; ++i) { + self->U[i] += self->D[d_index++] * self->V[ch][v_index++]; + } + + v_index += 128 - 32; + d_index += 64 - 32; + } + + // Output samples + #ifdef PLM_AUDIO_SEPARATE_CHANNELS + float *out_channel = ch == 0 + ? self->samples.left + : self->samples.right; + for (int j = 0; j < 32; j++) { + out_channel[out_pos + j] = self->U[j] / 2147418112.0f; + } + #else + for (int j = 0; j < 32; j++) { + self->samples.interleaved[((out_pos + j) << 1) + ch] = + self->U[j] / 2147418112.0f; + } + #endif + } // End of synthesis channel loop + out_pos += 32; + } // End of synthesis sub-block loop + + } // Decoding of the granule finished + } + + plm_buffer_align(self->buffer); +} + +const plm_quantizer_spec_t *plm_audio_read_allocation(plm_audio_t *self, int sb, int tab3) { + int tab4 = PLM_AUDIO_QUANT_LUT_STEP_3[tab3][sb]; + int qtab = PLM_AUDIO_QUANT_LUT_STEP_4[tab4 & 15][plm_buffer_read(self->buffer, tab4 >> 4)]; + return qtab ? (&PLM_AUDIO_QUANT_TAB[qtab - 1]) : 0; +} + +void plm_audio_read_samples(plm_audio_t *self, int ch, int sb, int part) { + const plm_quantizer_spec_t *q = self->allocation[ch][sb]; + int sf = self->scale_factor[ch][sb][part]; + int *sample = self->sample[ch][sb]; + int val = 0; + + if (!q) { + // No bits allocated for this subband + sample[0] = sample[1] = sample[2] = 0; + return; + } + + // Resolve scalefactor + if (sf == 63) { + sf = 0; + } + else { + int shift = (sf / 3) | 0; + sf = (PLM_AUDIO_SCALEFACTOR_BASE[sf % 3] + ((1 << shift) >> 1)) >> shift; + } + + // Decode samples + int adj = q->levels; + if (q->group) { + // Decode grouped samples + val = plm_buffer_read(self->buffer, q->bits); + sample[0] = val % adj; + val /= adj; + sample[1] = val % adj; + sample[2] = val / adj; + } + else { + // Decode direct samples + sample[0] = plm_buffer_read(self->buffer, q->bits); + sample[1] = plm_buffer_read(self->buffer, q->bits); + sample[2] = plm_buffer_read(self->buffer, q->bits); + } + + // Postmultiply samples + int scale = 65536 / (adj + 1); + adj = ((adj + 1) >> 1) - 1; + + val = (adj - sample[0]) * scale; + sample[0] = (val * (sf >> 12) + ((val * (sf & 4095) + 2048) >> 12)) >> 12; + + val = (adj - sample[1]) * scale; + sample[1] = (val * (sf >> 12) + ((val * (sf & 4095) + 2048) >> 12)) >> 12; + + val = (adj - sample[2]) * scale; + sample[2] = (val * (sf >> 12) + ((val * (sf & 4095) + 2048) >> 12)) >> 12; +} + +void plm_audio_idct36(int s[32][3], int ss, float *d, int dp) { + float t01, t02, t03, t04, t05, t06, t07, t08, t09, t10, t11, t12, + t13, t14, t15, t16, t17, t18, t19, t20, t21, t22, t23, t24, + t25, t26, t27, t28, t29, t30, t31, t32, t33; + + t01 = (float)(s[0][ss] + s[31][ss]); t02 = (float)(s[0][ss] - s[31][ss]) * 0.500602998235f; + t03 = (float)(s[1][ss] + s[30][ss]); t04 = (float)(s[1][ss] - s[30][ss]) * 0.505470959898f; + t05 = (float)(s[2][ss] + s[29][ss]); t06 = (float)(s[2][ss] - s[29][ss]) * 0.515447309923f; + t07 = (float)(s[3][ss] + s[28][ss]); t08 = (float)(s[3][ss] - s[28][ss]) * 0.53104259109f; + t09 = (float)(s[4][ss] + s[27][ss]); t10 = (float)(s[4][ss] - s[27][ss]) * 0.553103896034f; + t11 = (float)(s[5][ss] + s[26][ss]); t12 = (float)(s[5][ss] - s[26][ss]) * 0.582934968206f; + t13 = (float)(s[6][ss] + s[25][ss]); t14 = (float)(s[6][ss] - s[25][ss]) * 0.622504123036f; + t15 = (float)(s[7][ss] + s[24][ss]); t16 = (float)(s[7][ss] - s[24][ss]) * 0.674808341455f; + t17 = (float)(s[8][ss] + s[23][ss]); t18 = (float)(s[8][ss] - s[23][ss]) * 0.744536271002f; + t19 = (float)(s[9][ss] + s[22][ss]); t20 = (float)(s[9][ss] - s[22][ss]) * 0.839349645416f; + t21 = (float)(s[10][ss] + s[21][ss]); t22 = (float)(s[10][ss] - s[21][ss]) * 0.972568237862f; + t23 = (float)(s[11][ss] + s[20][ss]); t24 = (float)(s[11][ss] - s[20][ss]) * 1.16943993343f; + t25 = (float)(s[12][ss] + s[19][ss]); t26 = (float)(s[12][ss] - s[19][ss]) * 1.48416461631f; + t27 = (float)(s[13][ss] + s[18][ss]); t28 = (float)(s[13][ss] - s[18][ss]) * 2.05778100995f; + t29 = (float)(s[14][ss] + s[17][ss]); t30 = (float)(s[14][ss] - s[17][ss]) * 3.40760841847f; + t31 = (float)(s[15][ss] + s[16][ss]); t32 = (float)(s[15][ss] - s[16][ss]) * 10.1900081235f; + + t33 = t01 + t31; t31 = (t01 - t31) * 0.502419286188f; + t01 = t03 + t29; t29 = (t03 - t29) * 0.52249861494f; + t03 = t05 + t27; t27 = (t05 - t27) * 0.566944034816f; + t05 = t07 + t25; t25 = (t07 - t25) * 0.64682178336f; + t07 = t09 + t23; t23 = (t09 - t23) * 0.788154623451f; + t09 = t11 + t21; t21 = (t11 - t21) * 1.06067768599f; + t11 = t13 + t19; t19 = (t13 - t19) * 1.72244709824f; + t13 = t15 + t17; t17 = (t15 - t17) * 5.10114861869f; + t15 = t33 + t13; t13 = (t33 - t13) * 0.509795579104f; + t33 = t01 + t11; t01 = (t01 - t11) * 0.601344886935f; + t11 = t03 + t09; t09 = (t03 - t09) * 0.899976223136f; + t03 = t05 + t07; t07 = (t05 - t07) * 2.56291544774f; + t05 = t15 + t03; t15 = (t15 - t03) * 0.541196100146f; + t03 = t33 + t11; t11 = (t33 - t11) * 1.30656296488f; + t33 = t05 + t03; t05 = (t05 - t03) * 0.707106781187f; + t03 = t15 + t11; t15 = (t15 - t11) * 0.707106781187f; + t03 += t15; + t11 = t13 + t07; t13 = (t13 - t07) * 0.541196100146f; + t07 = t01 + t09; t09 = (t01 - t09) * 1.30656296488f; + t01 = t11 + t07; t07 = (t11 - t07) * 0.707106781187f; + t11 = t13 + t09; t13 = (t13 - t09) * 0.707106781187f; + t11 += t13; t01 += t11; + t11 += t07; t07 += t13; + t09 = t31 + t17; t31 = (t31 - t17) * 0.509795579104f; + t17 = t29 + t19; t29 = (t29 - t19) * 0.601344886935f; + t19 = t27 + t21; t21 = (t27 - t21) * 0.899976223136f; + t27 = t25 + t23; t23 = (t25 - t23) * 2.56291544774f; + t25 = t09 + t27; t09 = (t09 - t27) * 0.541196100146f; + t27 = t17 + t19; t19 = (t17 - t19) * 1.30656296488f; + t17 = t25 + t27; t27 = (t25 - t27) * 0.707106781187f; + t25 = t09 + t19; t19 = (t09 - t19) * 0.707106781187f; + t25 += t19; + t09 = t31 + t23; t31 = (t31 - t23) * 0.541196100146f; + t23 = t29 + t21; t21 = (t29 - t21) * 1.30656296488f; + t29 = t09 + t23; t23 = (t09 - t23) * 0.707106781187f; + t09 = t31 + t21; t31 = (t31 - t21) * 0.707106781187f; + t09 += t31; t29 += t09; t09 += t23; t23 += t31; + t17 += t29; t29 += t25; t25 += t09; t09 += t27; + t27 += t23; t23 += t19; t19 += t31; + t21 = t02 + t32; t02 = (t02 - t32) * 0.502419286188f; + t32 = t04 + t30; t04 = (t04 - t30) * 0.52249861494f; + t30 = t06 + t28; t28 = (t06 - t28) * 0.566944034816f; + t06 = t08 + t26; t08 = (t08 - t26) * 0.64682178336f; + t26 = t10 + t24; t10 = (t10 - t24) * 0.788154623451f; + t24 = t12 + t22; t22 = (t12 - t22) * 1.06067768599f; + t12 = t14 + t20; t20 = (t14 - t20) * 1.72244709824f; + t14 = t16 + t18; t16 = (t16 - t18) * 5.10114861869f; + t18 = t21 + t14; t14 = (t21 - t14) * 0.509795579104f; + t21 = t32 + t12; t32 = (t32 - t12) * 0.601344886935f; + t12 = t30 + t24; t24 = (t30 - t24) * 0.899976223136f; + t30 = t06 + t26; t26 = (t06 - t26) * 2.56291544774f; + t06 = t18 + t30; t18 = (t18 - t30) * 0.541196100146f; + t30 = t21 + t12; t12 = (t21 - t12) * 1.30656296488f; + t21 = t06 + t30; t30 = (t06 - t30) * 0.707106781187f; + t06 = t18 + t12; t12 = (t18 - t12) * 0.707106781187f; + t06 += t12; + t18 = t14 + t26; t26 = (t14 - t26) * 0.541196100146f; + t14 = t32 + t24; t24 = (t32 - t24) * 1.30656296488f; + t32 = t18 + t14; t14 = (t18 - t14) * 0.707106781187f; + t18 = t26 + t24; t24 = (t26 - t24) * 0.707106781187f; + t18 += t24; t32 += t18; + t18 += t14; t26 = t14 + t24; + t14 = t02 + t16; t02 = (t02 - t16) * 0.509795579104f; + t16 = t04 + t20; t04 = (t04 - t20) * 0.601344886935f; + t20 = t28 + t22; t22 = (t28 - t22) * 0.899976223136f; + t28 = t08 + t10; t10 = (t08 - t10) * 2.56291544774f; + t08 = t14 + t28; t14 = (t14 - t28) * 0.541196100146f; + t28 = t16 + t20; t20 = (t16 - t20) * 1.30656296488f; + t16 = t08 + t28; t28 = (t08 - t28) * 0.707106781187f; + t08 = t14 + t20; t20 = (t14 - t20) * 0.707106781187f; + t08 += t20; + t14 = t02 + t10; t02 = (t02 - t10) * 0.541196100146f; + t10 = t04 + t22; t22 = (t04 - t22) * 1.30656296488f; + t04 = t14 + t10; t10 = (t14 - t10) * 0.707106781187f; + t14 = t02 + t22; t02 = (t02 - t22) * 0.707106781187f; + t14 += t02; t04 += t14; t14 += t10; t10 += t02; + t16 += t04; t04 += t08; t08 += t14; t14 += t28; + t28 += t10; t10 += t20; t20 += t02; t21 += t16; + t16 += t32; t32 += t04; t04 += t06; t06 += t08; + t08 += t18; t18 += t14; t14 += t30; t30 += t28; + t28 += t26; t26 += t10; t10 += t12; t12 += t20; + t20 += t24; t24 += t02; + + d[dp + 48] = -t33; + d[dp + 49] = d[dp + 47] = -t21; + d[dp + 50] = d[dp + 46] = -t17; + d[dp + 51] = d[dp + 45] = -t16; + d[dp + 52] = d[dp + 44] = -t01; + d[dp + 53] = d[dp + 43] = -t32; + d[dp + 54] = d[dp + 42] = -t29; + d[dp + 55] = d[dp + 41] = -t04; + d[dp + 56] = d[dp + 40] = -t03; + d[dp + 57] = d[dp + 39] = -t06; + d[dp + 58] = d[dp + 38] = -t25; + d[dp + 59] = d[dp + 37] = -t08; + d[dp + 60] = d[dp + 36] = -t11; + d[dp + 61] = d[dp + 35] = -t18; + d[dp + 62] = d[dp + 34] = -t09; + d[dp + 63] = d[dp + 33] = -t14; + d[dp + 32] = -t05; + d[dp + 0] = t05; d[dp + 31] = -t30; + d[dp + 1] = t30; d[dp + 30] = -t27; + d[dp + 2] = t27; d[dp + 29] = -t28; + d[dp + 3] = t28; d[dp + 28] = -t07; + d[dp + 4] = t07; d[dp + 27] = -t26; + d[dp + 5] = t26; d[dp + 26] = -t23; + d[dp + 6] = t23; d[dp + 25] = -t10; + d[dp + 7] = t10; d[dp + 24] = -t15; + d[dp + 8] = t15; d[dp + 23] = -t12; + d[dp + 9] = t12; d[dp + 22] = -t19; + d[dp + 10] = t19; d[dp + 21] = -t20; + d[dp + 11] = t20; d[dp + 20] = -t13; + d[dp + 12] = t13; d[dp + 19] = -t24; + d[dp + 13] = t24; d[dp + 18] = -t31; + d[dp + 14] = t31; d[dp + 17] = -t02; + d[dp + 15] = t02; d[dp + 16] = 0.0; +} + + +#endif // PL_MPEG_IMPLEMENTATION diff --git a/runner.cpp b/runner.cpp new file mode 100644 index 0000000..00858c4 --- /dev/null +++ b/runner.cpp @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: LGPL-2.0-or-later + +#define INITGUID +#define STRSAFE_NO_DEPRECATE +#include +#include +#include +#include +#include + +static const GUID GUID_NULL = {}; // not defined in my headers, how lovely +DEFINE_GUID(CLSID_decodebin_parser, 0xf9d8d64e, 0xa144, 0x47dc, 0x8e, 0xe0, 0xf5, 0x34, 0x98, 0x37, 0x2c, 0x29); + +static char* guid_to_str(const GUID& guid) +{ + static char buf[8][64]; + static int n = 0; + char* ret = buf[n++%8]; + sprintf(ret, "{%08lX-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}", guid.Data1, guid.Data2, guid.Data3, guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3], guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]); + return ret; +} +static wchar_t* guid_to_wstr(const GUID& guid) +{ + static wchar_t buf[8][64] = {}; + char* guids = guid_to_str(guid); + static int n = 0; + wchar_t* ret = buf[n++%8]; + for (int i=0;guids[i];i++) + ret[i] = guids[i]; + return ret; +} + +template class CComPtr { + void assign(T* ptr) + { + p = ptr; + } + void release() + { + if (p) + p->Release(); + p = nullptr; + } +public: + T* p; + + CComPtr() { p = nullptr; } + ~CComPtr() { release(); } + CComPtr(const CComPtr&) = delete; + CComPtr(CComPtr&&) = delete; + void operator=(const CComPtr&) = delete; + void operator=(CComPtr&&) = delete; + + CComPtr& operator=(T* ptr) + { + release(); + assign(ptr); + return *this; + } + T** operator&() + { + release(); + return &p; + } + T* operator->() { return p; } + operator T*() { return p; } + + HRESULT CoCreateInstance(REFCLSID rclsid, LPUNKNOWN pUnkOuter, DWORD dwClsContext) + { + release(); + return ::CoCreateInstance(rclsid, pUnkOuter, dwClsContext, IID_PPV_ARGS(&p)); + } + template + HRESULT QueryInterface(T2** other) + { + return p->QueryInterface(IID_PPV_ARGS(other)); + } +}; + +static HRESULT connect_filters(IGraphBuilder* graph, IBaseFilter* src, IBaseFilter* dst) +{ +puts("connect."); + CComPtr src_enum; + if (FAILED(src->EnumPins(&src_enum))) + return E_FAIL; + + CComPtr src_pin; + while (src_enum->Next(1, &src_pin, nullptr) == S_OK) + { +puts("src."); + PIN_INFO src_info; + if (FAILED(src_pin->QueryPinInfo(&src_info))) + return E_FAIL; + if (src_info.pFilter) + src_info.pFilter->Release(); + if (src_info.dir != PINDIR_OUTPUT) + continue; + + CComPtr check_pin; + src_pin->ConnectedTo(&check_pin); + if (check_pin != nullptr) + continue; + + CComPtr dst_enum; + dst->EnumPins(&dst_enum); + CComPtr dst_pin; + while (dst_enum->Next(1, &dst_pin, nullptr) == S_OK) + { +puts("dst."); + PIN_INFO dst_info; + if (FAILED(dst_pin->QueryPinInfo(&dst_info))) + return E_FAIL; + if (dst_info.pFilter) + dst_info.pFilter->Release(); + if (dst_info.dir != PINDIR_INPUT) + continue; + + dst_pin->ConnectedTo(&check_pin); + if (check_pin != nullptr) + continue; + +puts("match."); + //if (SUCCEEDED(graph->Connect(src_pin, dst_pin))) + if (SUCCEEDED(graph->ConnectDirect(src_pin, dst_pin, nullptr))) + { +puts("match2."); + return S_OK; + } +puts("match3."); + } + } + return E_FAIL; +} + +static void require(int seq, HRESULT hr, const char * text = "") +{ + if (SUCCEEDED(hr)) + return; + + printf("fail: %d %.8lX%s%s\n", seq, hr, text?" ":"", text); + Sleep(5000); + exit(seq); +} + + + +static CComPtr filterGraph; + +IBaseFilter* try_make_filter(REFIID riid) +{ + IBaseFilter* filt = nullptr; + CoCreateInstance(riid, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&filt)); + if (!filt) + return nullptr; + filterGraph->AddFilter(filt, guid_to_wstr(riid)); + return filt; +} +IBaseFilter* make_filter(IBaseFilter* filt) { return filt; } +IBaseFilter* make_filter(REFIID riid) +{ + IBaseFilter* filt = try_make_filter(riid); + if (!filt) + { + printf("failed to create %s\n", guid_to_str(riid)); + Sleep(5000); + exit(1); + } + return filt; +} +bool connect_chain(IBaseFilter** filts, size_t n, bool required) +{ + for (size_t i=0;iQueryFilterInfo(&inf1); + second->QueryFilterInfo(&inf2); + printf("failed to connect %ls to %ls\n", inf1.achName, inf2.achName); + Sleep(5000); + exit(1); + } + } + return true; +} +template +IBaseFilter* chain_tail(Ts... args) +{ + IBaseFilter* filts[] = { make_filter(args)... }; + connect_chain(filts, sizeof...(Ts), true); + return filts[sizeof...(Ts)-1]; +} +template +void chain(Ts... args) +{ + IBaseFilter* filts[] = { make_filter(args)... }; + connect_chain(filts, sizeof...(Ts), true); +} +template +bool try_chain(Ts... args) +{ + IBaseFilter* filts[] = { make_filter(args)... }; + return connect_chain(filts, sizeof...(Ts), false); +} + +int main() +{ + setvbuf(stdout, nullptr, _IONBF, 0); + CoInitializeEx(NULL, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE); + + filterGraph.CoCreateInstance(CLSID_FilterGraph, nullptr, CLSCTX_INPROC_SERVER); + + IBaseFilter* asyncReader = make_filter(CLSID_AsyncReader); + CComPtr asyncReaderFsf; + require(20, asyncReader->QueryInterface(&asyncReaderFsf)); + require(21, asyncReaderFsf->Load(L"video.mpg", nullptr), "failed to open video.mpg, does the file exist?"); + //require(21, asyncReaderFsf->Load(L"Video1.WMV", nullptr), "failed to open Video1.WMV, does the file exist?"); + + IBaseFilter* mpegdec = try_make_filter(CLSID_CMpegVideoCodec); +//mpegdec = nullptr; // uncomment to force decodebin + bool audio; + if (mpegdec) + { + IBaseFilter* demux = chain_tail(asyncReader, CLSID_MPEG1Splitter); + chain(demux, mpegdec, CLSID_VideoMixingRenderer9); + audio = try_chain(demux, CLSID_CMpegAudioCodec, CLSID_DSoundRender); // don't worry too much if the file doesn't have sound + } + else + { + puts("CLSID_CMpegVideoCodec not available? Probably running on Wine, trying CLSID_decodebin_parser instead"); + IBaseFilter* demux = chain_tail(asyncReader, CLSID_decodebin_parser); +puts("a"); + chain(demux, CLSID_VideoMixingRenderer9); +puts("b"); + audio = try_chain(demux, CLSID_DSoundRender); +puts("c"); + } + if (!audio) + puts("no audio??"); + + puts("connected"); + + CComPtr filterGraph_mc; + require(40, filterGraph.QueryInterface(&filterGraph_mc)); + filterGraph_mc->Run(); + puts("running"); + SleepEx(10000, true); +} diff --git a/x/d3d9-on-child-window.cpp b/x/d3d9-on-child-window.cpp new file mode 100644 index 0000000..59e7eaf --- /dev/null +++ b/x/d3d9-on-child-window.cpp @@ -0,0 +1,72 @@ +#include +#include +#include +#include + +HWND wnd_parent; +HWND wnd; +int f; + +LRESULT myWindowProcA(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) +{ +printf("%d %u %d\n", hWnd==wnd, Msg, f++); + if (Msg == WM_CLOSE) + exit(0); + return DefWindowProcA(hWnd, Msg, wParam, lParam); +} + +int main() +{ + WNDCLASSEXA wcex = { sizeof(WNDCLASSEX), CS_DBLCLKS, myWindowProcA, 0L, 0L, nullptr, NULL, NULL, NULL, NULL, "windowclass", NULL }; + RegisterClassEx(&wcex); + + WNDCLASSEXA wcex2 = { sizeof(WNDCLASSEX), 0, myWindowProcA, 0L, 0L, nullptr, NULL, NULL, NULL, NULL, "windowclass_child", NULL }; + RegisterClassEx(&wcex2); + + uint32_t wstyle = WS_CAPTION|WS_CLIPSIBLINGS|WS_CLIPCHILDREN|WS_VISIBLE|WS_SYSMENU|WS_MINIMIZEBOX; + uint32_t wstyleex = WS_EX_ACCEPTFILES|WS_EX_WINDOWEDGE|WS_EX_CONTROLPARENT; + wnd_parent = CreateWindowExA(wstyleex, "windowclass", "parent", wstyle, 0, 0, 640, 480, nullptr, NULL, nullptr, NULL ); + ShowWindow(wnd_parent, SW_SHOWDEFAULT); + UpdateWindow(wnd_parent); + + wnd = CreateWindowExA(0, "windowclass_child", "child", WS_CHILD, 0, 0, 320, 240, wnd_parent, NULL, nullptr, NULL ); + ShowWindow(wnd, SW_SHOWDEFAULT); + UpdateWindow(wnd); + + IDirect3D9* d3d = Direct3DCreate9(D3D_SDK_VERSION); + DWORD BehaviorFlags = D3DCREATE_SOFTWARE_VERTEXPROCESSING | D3DCREATE_MULTITHREADED; + + D3DPRESENT_PARAMETERS d3dpp = {}; + D3DDISPLAYMODE dm; + d3d->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &dm); + d3dpp.Windowed = TRUE; + d3dpp.SwapEffect = D3DSWAPEFFECT_COPY; + d3dpp.BackBufferFormat = dm.Format; + d3dpp.BackBufferHeight = 640; + d3dpp.BackBufferWidth = 480; + d3dpp.hDeviceWindow = wnd_parent; + + IDirect3DDevice9* d3ddev; + d3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, NULL, BehaviorFlags, &d3dpp, &d3ddev); + + IDirect3D9* d3d2 = Direct3DCreate9(D3D_SDK_VERSION); + d3dpp.hDeviceWindow = wnd; + IDirect3DDevice9* d3ddev2; + d3d2->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, NULL, BehaviorFlags, &d3dpp, &d3ddev2); + + for (int frame=0;frame<600;frame++) + { + d3ddev->Clear(0,NULL,D3DCLEAR_TARGET,D3DCOLOR_RGBA(255,0,frame*4,0),1.0f,0); + d3ddev->Present( nullptr, NULL, nullptr, NULL ); + + d3ddev2->Clear(0,NULL,D3DCLEAR_TARGET,D3DCOLOR_RGBA(0,255,frame*4,0),1.0f,0); + d3ddev2->Present( nullptr, NULL, nullptr, NULL ); + + MSG msg; + while( (PeekMessage( &msg, NULL, 0, 0, PM_REMOVE )) != 0) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } +} diff --git a/x/gstkrkr-demux-video.c b/x/gstkrkr-demux-video.c new file mode 100644 index 0000000..f47b37f --- /dev/null +++ b/x/gstkrkr-demux-video.c @@ -0,0 +1,722 @@ +// SPDX-License-Identifier: LGPL-2.0-or-later + +#include +#include +#define PL_MPEG_IMPLEMENTATION +#include "pl_mpeg.h" +#include "mpeg_packet.h" + +/** + * SECTION:element-plugin + * + * FIXME:Describe plugin here. + * + * + * Example launch line + * |[ + * gst-launch-1.0 filesrc location=video.mpg ! mpegpsdemux name=demux ! mpegvideoparse ! avdec_mpeg2video ! queue ! autovideosink \ + * demux. ! mpegaudioparse ! avdec_mp2float ! audioconvert ! queue ! autoaudiosink + * gst-launch-1.0 filesrc location=video.mpg ! krkr_demux name=demux ! krkr_video ! queue ! autovideosink \ + * demux. ! avdec_mp2float ! audioconvert ! queue ! autoaudiosink + * ]| + * + */ + +GST_DEBUG_CATEGORY_STATIC(gst_krkr_debug); +#define GST_CAT_DEFAULT gst_krkr_debug + +// in GStreamer, decoding a mpeg video requires three elements: mpegpsdemux ! mpegvideoparse ! avdec_mpeg2video +// in pl_mpeg, the latter two are merged to one object +// in DirectShow, the FORMER two are merged to one object +// (similar for audio, the elements are named mpegpsdemux ! mpegaudioparse ! avdec_mp2float) + +// In both cases, the middle element takes arbitrary-sized chunks, adds video size and sample rate and stuff to the output pad, +// discards corrupted data, and returns proper packets. +// Wine's CLSID_CMpegAudioCodec corresponds to avdec_mp2float only, so I implemented my own mpegaudioparse. +// It's trivial (other than error recovery, which I just ignore - file corruption isn't a thing anymore). +// However, mpegvideoparse is complicated, and Wine doesn't implement CLSID_CMpegVideoCodec, +// so I chose to split data differently. + +// rank rules: Must be higher than media-converter, and lower than every relevant official GStreamer filter. +// The relevant filters are +// - protonaudioconverter MARGINAL audio/x-wma +// - protonaudioconverterbin MARGINAL+1 audio/x-wma +// - protonvideoconverter MARGINAL video/x-ms-asf, video/x-msvideo, video/mpeg, video/quicktime +// - mpegpsdemux PRIMARY video/mpeg(systemstream=true) +// - mpegvideoparse PRIMARY+1 video/mpeg(systemstream=false) +// - avdec_mpeg2video PRIMARY video/mpeg(mpegversion=[1,2], systemstream=false, parsed=false) +// - mpegaudioparse PRIMARY+2 audio/mpeg(mpegversion=1, layer=2) +// - avdec_mp2float MARGINAL audio/mpeg(mpegversion=1, layer=2, parsed=true) +// - asfdemux SECONDARY video/x-ms-asf +// - avdec_wma* MARGINAL audio/x-wma +// - avdec_wmv* MARGINAL video/x-wmv + +// This means I must use at least rank MARGINAL+2 for audio/x-wma, and MARGINAL+1 for the others. +// However, I also need to rank below avdec_wma*'s MARGINAL. Obviously, that's impossible. +// To solve this, I will create two elements; krkrwma and krkrwmaauto. +// Former is a normal decoder, with rank MARGINAL-1. Latter has rank MARGINAL+2, and is just a wrapper thing. +// Upon creation, it reads the GStreamer registry and looks for anything else that can read audio/wma. +// It ignores itself, subtracts 16 points from protonaudioconverter and protonaudioconverterbin, +// and returns the one with highest adjusted rank. +// Ideally, krkrwmaauto wouldn't even exist if protonaudioconverter doesn't, but that's just infeasible. +// avdec_mp2float is also MARGINAL, but neither I nor protonaudioconverter accept audio/mpeg, so there's no problem there. + +// Sources are mostly gst-inspect-1.0; some source code is available at +// +// +// +// +// + +//#define DEMUX_RANK GST_RANK_MARGINAL+2 +//#define VIDEO_RANK GST_RANK_MARGINAL+2 + +#define DEMUX_RANK GST_RANK_PRIMARY+2 +#define VIDEO_RANK GST_RANK_PRIMARY+2 + +static void print_event(const char * pad_name, GstEvent* event) +{ + //return; + fprintf(stderr, "gstkrkr: Received %s event on %s\n", GST_EVENT_TYPE_NAME(event), pad_name); + return; + gst_print("gstkrkr: Received %s event on %s: ", GST_EVENT_TYPE_NAME(event), pad_name); + + switch (GST_EVENT_TYPE(event)) + { + case GST_EVENT_CAPS: + { + GstCaps* caps; + gst_event_parse_caps(event, &caps); + gst_print("%" GST_PTR_FORMAT, caps); + break; + } + case GST_EVENT_SEGMENT: + { + const GstSegment * segment; + gst_event_parse_segment(event, &segment); + gst_print("%lu/%lu\n", (unsigned long)segment->position, (unsigned long)segment->duration); + break; + } + case GST_EVENT_TAG: + { + GstTagList* taglist; + gst_event_parse_tag(event, &taglist); + gst_print("%" GST_PTR_FORMAT, taglist); + break; + } + default: + gst_print("(unknown type)"); + } +} + +static void print_query(const char * pad_name, GstQuery* query) +{ + //return; + fprintf(stderr, "gstkrkr: Received %s query on %s\n", GST_QUERY_TYPE_NAME(query), pad_name); +} + +// same as gst_buffer_new_memdup, except that function is new in GStreamer 1.20; Proton's is version 1.18.5 +static GstBuffer* wrap_bytes_to_gst(const uint8_t * buf, size_t size) +{ + GBytes* by = g_bytes_new(buf, size); + GstBuffer* ret = gst_buffer_new_wrapped_bytes(by); + g_bytes_unref(by); + return ret; +} + + + + + +#define GST_TYPE_PLMPEG_DEMUX (gst_krkr_demux_get_type()) +G_DECLARE_FINAL_TYPE(GstPlMpegDemux, gst_krkr_demux, GST, PLMPEG_DEMUX, GstElement) + +struct _GstPlMpegDemux +{ + GstElement element; + + GstPad* sinkpad; + GstPad* videopad; + GstPad* audiopad; + + plm_buffer_t* buf; + plm_demux_t* demux; + + size_t audio_chunk_pos; + uint8_t audio_chunk[MP2_PACKET_MAX_SIZE]; +}; + +static GstStaticPadTemplate demux_sink_factory = GST_STATIC_PAD_TEMPLATE( + "sink", + GST_PAD_SINK, + GST_PAD_ALWAYS, + GST_STATIC_CAPS("video/mpeg, mpegversion=(int)1, systemstream=(boolean)true") + ); + +static GstStaticPadTemplate demux_videosrc_factory = GST_STATIC_PAD_TEMPLATE( + "video", + GST_PAD_SRC, + GST_PAD_SOMETIMES, + GST_STATIC_CAPS("video/mpeg, mpegversion=(int)1, systemstream=(boolean)false, parsed=(boolean)true, pixel-aspect-ratio=(fraction)1/1") + //video/mpeg, width=(int)640, height=(int)360, framerate=(fraction)30000/1001, codec_data=(buffer)000001b328016814ffffe018 + ); + +static GstStaticPadTemplate demux_audiosrc_factory = GST_STATIC_PAD_TEMPLATE( + "audio", + GST_PAD_SRC, + GST_PAD_SOMETIMES, + GST_STATIC_CAPS("audio/mpeg, mpegversion=(int)1, mpegaudioversion=(int)1, layer=(int)2, parsed=(boolean)true") + //audio/mpeg, mpegversion=(int)1, mpegaudioversion=(int)1, layer=(int)2, rate=(int)44100, channels=(int)2, parsed=(boolean)true + ); + +G_DEFINE_TYPE(GstPlMpegDemux, gst_krkr_demux, GST_TYPE_ELEMENT); + +GST_ELEMENT_REGISTER_DECLARE(krkr_demux); +GST_ELEMENT_REGISTER_DEFINE(krkr_demux, "krkr_demux", DEMUX_RANK, GST_TYPE_PLMPEG_DEMUX); + +static void gst_krkr_demux_finalize(GObject* object); + +static gboolean gst_krkr_demux_sink_event(GstPad* pad, GstObject* parent, GstEvent* event); +static GstFlowReturn gst_krkr_demux_sink_chain(GstPad* pad, GstObject* parent, GstBuffer* buf); +static gboolean gst_krkr_demux_videosrc_event(GstPad* pad, GstObject* parent, GstEvent* event); +static gboolean gst_krkr_demux_audiosrc_event(GstPad* pad, GstObject* parent, GstEvent* event); +static gboolean gst_krkr_demux_videosrc_query(GstPad* pad, GstObject* parent, GstQuery* query); +static gboolean gst_krkr_demux_audiosrc_query(GstPad* pad, GstObject* parent, GstQuery* query); + +static void gst_krkr_demux_class_init(GstPlMpegDemuxClass* klass) +{ + GObjectClass* gobject_class; + GstElementClass* gstelement_class; + + gobject_class = (GObjectClass*)klass; + gstelement_class = (GstElementClass*)klass; + + gobject_class->finalize = gst_krkr_demux_finalize; + + gst_element_class_set_details_simple(gstelement_class, + "krkr_demux", + "Demuxer", + "MPEG-1 demuxer", + "Sir Walrus sir@walrus.se"); + + gst_element_class_add_pad_template(gstelement_class, gst_static_pad_template_get(&demux_sink_factory)); + gst_element_class_add_pad_template(gstelement_class, gst_static_pad_template_get(&demux_videosrc_factory)); + gst_element_class_add_pad_template(gstelement_class, gst_static_pad_template_get(&demux_audiosrc_factory)); +} + +static void gst_krkr_demux_init(GstPlMpegDemux* filter) +{ + filter->sinkpad = gst_pad_new_from_static_template(&demux_sink_factory, "sink"); + gst_pad_set_event_function(filter->sinkpad, GST_DEBUG_FUNCPTR(gst_krkr_demux_sink_event)); + gst_pad_set_chain_function(filter->sinkpad, GST_DEBUG_FUNCPTR(gst_krkr_demux_sink_chain)); + GST_OBJECT_FLAG_SET(filter->sinkpad, GST_PAD_FLAG_NEED_PARENT); + gst_element_add_pad(GST_ELEMENT(filter), filter->sinkpad); + + filter->videopad = NULL; + filter->audiopad = NULL; + + // plm_buffer_create_with_capacity would be more memory friendly, but I can't figure out how that interacts with seeking + filter->buf = plm_buffer_create_for_appending(PLM_BUFFER_DEFAULT_SIZE); + filter->demux = plm_demux_create(filter->buf, false); + + filter->audio_chunk_pos = 0; + + //fprintf(stderr, "gstkrkr: Created a demuxer\n"); +} + +static void gst_krkr_demux_finalize(GObject* object) +{ + GstPlMpegDemux* filter = GST_PLMPEG_DEMUX(object); + + // don't unref the pads, the floating ref was taken by gst_element_add_pad + + plm_buffer_destroy(filter->buf); + plm_demux_destroy(filter->demux); + + G_OBJECT_CLASS(gst_krkr_demux_parent_class)->finalize(object); +} + +static gboolean gst_krkr_demux_sink_event(GstPad* pad, GstObject* parent, GstEvent* event) +{ + print_event("demux sink", event); + + GstPlMpegDemux* filter = GST_PLMPEG_DEMUX(parent); + + switch (GST_EVENT_TYPE(event)) + { + case GST_EVENT_CAPS: + gst_event_unref(event); // don't care what we're given + return TRUE; + case GST_EVENT_EOS: + plm_buffer_signal_end(filter->buf); + gst_krkr_demux_sink_chain(pad, parent, NULL); + return TRUE; + default: + return gst_pad_event_default(pad, parent, event); + } +} + +static void gst_krkr_demux_process_audio_bytes(GstPlMpegDemux* filter, const uint8_t * buf, size_t len); +static void gst_krkr_demux_process_audio_packet(GstPlMpegDemux* filter, const uint8_t * buf, size_t len); + +static GstFlowReturn gst_krkr_demux_sink_chain(GstPad* pad, GstObject* parent, GstBuffer* buf) +{ + GstPlMpegDemux* filter = GST_PLMPEG_DEMUX(parent); + if (buf != NULL) + { + //fprintf(stderr, "gstkrkr: demux %lu bytes\n", gst_buffer_get_size(buf)); + for (size_t n=0;nbuf, meminf.data, meminf.size); + gst_memory_unref(mem); + } + } + + while (true) + { + plm_packet_t* pack = plm_demux_decode(filter->demux); + if (!pack) + break; + + if (pack->type == PLM_DEMUX_PACKET_VIDEO_1) + { + if (!filter->videopad) + { + plm_video_t* vid = plm_video_create_with_buffer(plm_buffer_create_with_memory(pack->data, pack->length, false), true); + int width = plm_video_get_width(vid); + int height = plm_video_get_height(vid); + double fps = plm_video_get_framerate(vid); + plm_video_destroy(vid); + + filter->videopad = gst_pad_new_from_static_template(&demux_videosrc_factory, "video"); + gst_pad_set_event_function(filter->videopad, GST_DEBUG_FUNCPTR(gst_krkr_demux_videosrc_event)); + gst_pad_set_query_function(filter->videopad, GST_DEBUG_FUNCPTR(gst_krkr_demux_videosrc_query)); + GST_OBJECT_FLAG_SET(filter->videopad, GST_PAD_FLAG_NEED_PARENT); + gst_element_add_pad(GST_ELEMENT(filter), filter->videopad); + + gst_pad_push_event(filter->videopad, gst_pad_get_sticky_event(filter->sinkpad, GST_EVENT_STREAM_START, 0)); + GstCaps* videocaps = gst_caps_new_simple("video/mpeg", + "mpegversion", G_TYPE_INT, 1, + "systemstream", G_TYPE_BOOLEAN, false, + "parsed", G_TYPE_BOOLEAN, true, + "width", G_TYPE_INT, width, + "height", G_TYPE_INT, height, + "pixel-aspect-ratio", GST_TYPE_FRACTION, 1, 1, + "framerate", GST_TYPE_FRACTION, (int)(fps*1000), 1000, + //"codec_data", GST_TYPE_BUFFER, NULL, // todo + NULL); + gst_pad_push_event(filter->videopad, gst_event_new_caps(videocaps)); + + GstSegment* seg = gst_segment_new(); + gst_segment_init(seg, GST_FORMAT_TIME); + // don't bother with duration, can't find one without reading the entire file and we don't have it yet + // (and calling plm_demux_get_duration does weird things to the current packet) + gst_pad_push_event(filter->videopad, gst_event_new_segment(seg)); + } + GstBuffer* buf = wrap_bytes_to_gst(pack->data, pack->length); + gst_pad_push(filter->videopad, buf); + } + if (pack->type == PLM_DEMUX_PACKET_AUDIO_1) + gst_krkr_demux_process_audio_bytes(filter, pack->data, pack->length); + } + + if (plm_demux_has_ended(filter->demux)) + { + if (filter->videopad != NULL) + gst_pad_push_event(filter->videopad, gst_event_new_eos()); + if (filter->audiopad != NULL) + { + if (filter->audio_chunk_pos > 0) + gst_krkr_demux_process_audio_packet(filter, filter->audio_chunk, filter->audio_chunk_pos); + gst_pad_push_event(filter->audiopad, gst_event_new_eos()); + } + return GST_FLOW_EOS; + } + + return GST_FLOW_OK; +} + +static size_t min_sz(size_t a, size_t b) { return a < b ? a : b; } + +static void gst_krkr_demux_process_audio_bytes(GstPlMpegDemux* filter, const uint8_t * buf, size_t len) +{ +again: + if (!len) + return; + + size_t claim = min_sz(len, MP2_PACKET_MAX_SIZE - filter->audio_chunk_pos); + memcpy(filter->audio_chunk + filter->audio_chunk_pos, buf, claim); + filter->audio_chunk_pos += claim; + buf += claim; + len -= claim; + + size_t pack_size = SIZE_MAX; + if (mp2_packet_parse(filter->audio_chunk, filter->audio_chunk_pos, NULL, NULL, &pack_size) < 0) + { + GST_ERROR("gstkrkr: corrupt data"); + return; + } + if (pack_size <= filter->audio_chunk_pos) + { + gst_krkr_demux_process_audio_packet(filter, filter->audio_chunk, pack_size); + memmove(filter->audio_chunk, filter->audio_chunk+pack_size, filter->audio_chunk_pos-pack_size); + filter->audio_chunk_pos -= pack_size; + } + goto again; +} + +static void gst_krkr_demux_process_audio_packet(GstPlMpegDemux* filter, const uint8_t * buf, size_t len) +{ + if (!filter->audiopad) + { + int channels; + int samplerate; + mp2_packet_parse(buf, len, &samplerate, &channels, NULL); + + filter->audiopad = gst_pad_new_from_static_template(&demux_audiosrc_factory, "audio"); + gst_pad_set_event_function(filter->audiopad, GST_DEBUG_FUNCPTR(gst_krkr_demux_audiosrc_event)); + gst_pad_set_query_function(filter->audiopad, GST_DEBUG_FUNCPTR(gst_krkr_demux_audiosrc_query)); + GST_OBJECT_FLAG_SET(filter->audiopad, GST_PAD_FLAG_NEED_PARENT); + gst_element_add_pad(GST_ELEMENT(filter), filter->audiopad); + + gst_pad_push_event(filter->audiopad, gst_pad_get_sticky_event(filter->sinkpad, GST_EVENT_STREAM_START, 0)); + GstCaps* audiocaps = gst_caps_new_simple("audio/mpeg", + "mpegversion", G_TYPE_INT, 1, + "mpegaudioversion", G_TYPE_INT, 1, + "layer", G_TYPE_INT, 2, + "parsed", G_TYPE_BOOLEAN, true, + "rate", G_TYPE_INT, samplerate, + "channels", G_TYPE_INT, channels, + NULL); + gst_pad_push_event(filter->audiopad, gst_event_new_caps(audiocaps)); + + GstSegment* seg = gst_segment_new(); + gst_segment_init(seg, GST_FORMAT_TIME); + gst_pad_push_event(filter->audiopad, gst_event_new_segment(seg)); + } + gst_pad_push(filter->audiopad, wrap_bytes_to_gst(buf, len)); +} + +static gboolean gst_krkr_demux_src_seek(GstPad* pad, GstPlMpegDemux* filter, GstEvent* event) +{ + double rate; // 1.0 + GstFormat format; // GST_FORMAT_TIME + GstSeekFlags flags; // GST_SEGMENT_FLAG_RESET + GstSeekType start_type; // GST_SEEK_TYPE_SET + int64_t start; // 0 + GstSeekType stop_type; // GST_SEEK_TYPE_NONE + int64_t stop; // 0 + gst_event_parse_seek(event, &rate, &format, &flags, &start_type, &start, &stop_type, &stop); + + if (start_type == GST_SEEK_TYPE_SET && start == 0) + { + plm_demux_rewind(filter->demux); + } + else if (format == GST_FORMAT_TIME) + { + int type = (filter->videopad ? PLM_DEMUX_PACKET_VIDEO_1 : PLM_DEMUX_PACKET_AUDIO_1); + plm_demux_seek(filter->demux, start / 1000000000.0, type, false); + } + else return FALSE; + return TRUE; +} + +static gboolean gst_krkr_demux_videosrc_event(GstPad* pad, GstObject* parent, GstEvent* event) +{ + GstPlMpegDemux* filter = GST_PLMPEG_DEMUX(parent); + + switch (GST_EVENT_TYPE(event)) { + case GST_EVENT_CAPS: + return TRUE; + case GST_EVENT_SEEK: + return gst_krkr_demux_src_seek(pad, filter, event); + default: + break; + } + return gst_pad_event_default(pad, parent, event); +} + +static gboolean gst_krkr_demux_audiosrc_event(GstPad* pad, GstObject* parent, GstEvent* event) +{ + GstPlMpegDemux* filter = GST_PLMPEG_DEMUX(parent); + + switch (GST_EVENT_TYPE(event)) { + case GST_EVENT_CAPS: + return TRUE; + case GST_EVENT_SEEK: + return gst_krkr_demux_src_seek(pad, filter, event); + default: + break; + } + return gst_pad_event_default(pad, parent, event); +} + +static gboolean gst_krkr_demux_src_query(GstPad* pad, GstObject* parent, GstQuery* query) +{ + switch (GST_QUERY_TYPE(query)) + { + case GST_QUERY_DURATION: + { + GstFormat fmt; + gst_query_parse_duration(query, &fmt, NULL); + if (fmt == GST_FORMAT_TIME) + { + gst_query_set_duration(query, GST_FORMAT_TIME, 5000000000); // 5 seconds + //gst_query_set_duration(query, GST_FORMAT_TIME, ); + } + return TRUE; + } + default: + break; + } + return gst_pad_query_default(pad, parent, query); +} + +static gboolean gst_krkr_demux_videosrc_query(GstPad* pad, GstObject* parent, GstQuery* query) +{ + print_query("demux videosrc", query); + return gst_krkr_demux_src_query(pad, parent, query); +} + +static gboolean gst_krkr_demux_audiosrc_query(GstPad* pad, GstObject* parent, GstQuery* query) +{ + print_query("demux audiosrc", query); + return gst_krkr_demux_src_query(pad, parent, query); +} + + + + + +#define GST_TYPE_PLMPEG_DECODE (gst_krkr_video_get_type()) +G_DECLARE_FINAL_TYPE(GstPlMpegDecode, gst_krkr_video, GST, PLMPEG_DECODE, GstElement) + +struct _GstPlMpegDecode +{ + GstElement element; + + GstPad* sinkpad; + GstPad* srcpad; + + int width; + int height; + + plm_buffer_t* buf; + plm_video_t* decode; +}; + +static GstStaticPadTemplate decode_sink_factory = GST_STATIC_PAD_TEMPLATE( + "sink", + GST_PAD_SINK, + GST_PAD_ALWAYS, + GST_STATIC_CAPS("video/mpeg, mpegversion=(int)1, systemstream=(boolean)false, parsed=(boolean)true") + ); + +static GstStaticPadTemplate decode_src_factory = GST_STATIC_PAD_TEMPLATE( + "src", + GST_PAD_SRC, + GST_PAD_ALWAYS, + GST_STATIC_CAPS("video/x-raw, format=(string)YV12") + ); + +G_DEFINE_TYPE(GstPlMpegDecode, gst_krkr_video, GST_TYPE_ELEMENT); + +GST_ELEMENT_REGISTER_DECLARE(krkr_video); +// same rank as the demuxer +GST_ELEMENT_REGISTER_DEFINE(krkr_video, "krkr_video", VIDEO_RANK, GST_TYPE_PLMPEG_DECODE); + +static void gst_krkr_video_finalize(GObject* object); + +static gboolean gst_krkr_video_sink_event(GstPad* pad, GstObject* parent, GstEvent* event); +static GstFlowReturn gst_krkr_video_sink_chain(GstPad* pad, GstObject* parent, GstBuffer* buf); +static gboolean gst_krkr_video_src_event(GstPad* pad, GstObject* parent, GstEvent* event); +static gboolean gst_krkr_video_src_query(GstPad* pad, GstObject* parent, GstQuery* query); + +static void gst_krkr_video_class_init(GstPlMpegDecodeClass* klass) +{ + GObjectClass* gobject_class; + GstElementClass* gstelement_class; + + gobject_class = (GObjectClass*)klass; + gstelement_class = (GstElementClass*)klass; + + gobject_class->finalize = gst_krkr_video_finalize; + + gst_element_class_set_details_simple(gstelement_class, + "krkr_video", + "Decoder/Video", + "MPEG-1 decoder", + "Sir Walrus sir@walrus.se"); + + gst_element_class_add_pad_template(gstelement_class, gst_static_pad_template_get(&decode_sink_factory)); + gst_element_class_add_pad_template(gstelement_class, gst_static_pad_template_get(&decode_src_factory)); +} + +static void gst_krkr_video_init(GstPlMpegDecode* filter) +{ + filter->sinkpad = gst_pad_new_from_static_template(&decode_sink_factory, "sink"); + gst_pad_set_event_function(filter->sinkpad, GST_DEBUG_FUNCPTR(gst_krkr_video_sink_event)); + gst_pad_set_chain_function(filter->sinkpad, GST_DEBUG_FUNCPTR(gst_krkr_video_sink_chain)); + GST_OBJECT_FLAG_SET(filter->sinkpad, GST_PAD_FLAG_NEED_PARENT); + gst_element_add_pad(GST_ELEMENT(filter), filter->sinkpad); + + filter->srcpad = gst_pad_new_from_static_template(&decode_src_factory, "src"); + gst_pad_set_event_function(filter->srcpad, GST_DEBUG_FUNCPTR(gst_krkr_video_src_event)); + gst_pad_set_query_function(filter->srcpad, GST_DEBUG_FUNCPTR(gst_krkr_video_src_query)); + GST_OBJECT_FLAG_SET(filter->srcpad, GST_PAD_FLAG_NEED_PARENT); + gst_element_add_pad(GST_ELEMENT(filter), filter->srcpad); + + filter->buf = plm_buffer_create_with_capacity(PLM_BUFFER_DEFAULT_SIZE); + filter->decode = plm_video_create_with_buffer(filter->buf, false); + + //fprintf(stderr, "gstkrkr: Created a decoder\n"); +} + +static void gst_krkr_video_finalize(GObject* object) +{ + GstPlMpegDecode* filter = GST_PLMPEG_DECODE(object); + + plm_buffer_destroy(filter->buf); + plm_video_destroy(filter->decode); + + G_OBJECT_CLASS(gst_krkr_video_parent_class)->finalize(object); +} + +static gboolean gst_krkr_video_sink_event(GstPad* pad, GstObject* parent, GstEvent* event) +{ + print_event("decode sink", event); + + GstPlMpegDecode* filter = GST_PLMPEG_DECODE(parent); + + switch (GST_EVENT_TYPE(event)) + { + case GST_EVENT_CAPS: + { + GstCaps* caps_in; + gst_event_parse_caps(event, &caps_in); + GstStructure* struc = gst_caps_get_structure(caps_in, 0); + + // expected input caps are + // video/mpeg, mpegversion=(int)1, systemstream=(boolean)false, parsed=(boolean)true, width=(int)640, height=(int)360, + // framerate=(fraction)30000/1001, pixel-aspect-ratio=(fraction)1/1, codec_data=(buffer)000001b328016814ffffe018 + + // output is + // video/x-raw, format=(string)YV12, width=(int)640, height=(int)360, interlace-mode=(string)progressive, + // pixel-aspect-ratio=(fraction)1/1, chroma-site=(string)jpeg, colorimetry=(string)2:0:0:0, framerate=(fraction)30000/1001 + gst_structure_get_int(struc, "width", &filter->width); + gst_structure_get_int(struc, "height", &filter->height); + int framerate_n; + int framerate_d; + gst_structure_get_fraction(struc, "framerate", &framerate_n, &framerate_d); + + GstCaps* caps_out = gst_caps_new_simple("video/x-raw", + "format", G_TYPE_STRING, "YV12", + "width", G_TYPE_INT, filter->width, + "height", G_TYPE_INT, filter->height, + "framerate", GST_TYPE_FRACTION, framerate_n, framerate_d, + NULL); + gst_pad_push_event(filter->srcpad, gst_event_new_caps(caps_out)); + return TRUE; + } + default: + return gst_pad_event_default(pad, parent, event); + } +} + +static GstFlowReturn gst_krkr_video_sink_chain(GstPad* pad, GstObject* parent, GstBuffer* buf) +{ + GstPlMpegDecode* filter = GST_PLMPEG_DECODE(parent); +//fprintf(stderr, "gstkrkr: decode %lu bytes, send %d\n", gst_buffer_get_size(buf), filter->should_send_frames); + for (size_t n=0;nbuf, meminf.data, meminf.size); + gst_memory_unref(mem); + } + + while (true) + { + plm_frame_t* frame = plm_video_decode(filter->decode); +//fprintf(stderr, "gstkrkr: decoded to %p\n", frame); + if (!frame) + break; + + size_t buflen = frame->width*frame->height*12/8; + uint8_t* ptr = g_malloc(buflen); + GstBuffer* buf = gst_buffer_new_wrapped(ptr, buflen); + for (int y=0;yheight;y++) + { + memcpy(ptr, frame->y.data + frame->y.width*y, frame->width); + ptr += frame->width; + } + for (int y=0;yheight/2;y++) + { + memcpy(ptr, frame->cr.data + frame->cr.width*y, frame->width/2); + ptr += frame->width/2; + } + for (int y=0;yheight/2;y++) + { + memcpy(ptr, frame->cb.data + frame->cb.width*y, frame->width/2); + ptr += frame->width/2; + } + + buf->pts = frame->time * 1000000000; + buf->dts = frame->time * 1000000000; + buf->duration = 1000000000 / plm_video_get_framerate(filter->decode); +//fprintf(stderr, "gstkrkr: send frame, %lu bytes\n", gst_buffer_get_size(buf)); + gst_pad_push(filter->srcpad, buf); + } + + return GST_FLOW_OK; +} + +static gboolean gst_krkr_video_src_event(GstPad* pad, GstObject* parent, GstEvent* event) +{ + print_event("decode source", event); + + //GstPlMpegDecode* filter = GST_PLMPEG_DECODE(parent); + + switch (GST_EVENT_TYPE(event)) { + case GST_EVENT_CAPS: + return TRUE; + default: + break; + } + return gst_pad_event_default(pad, parent, event); +} + +static gboolean gst_krkr_video_src_query(GstPad* pad, GstObject* parent, GstQuery* query) +{ + print_query("video src", query); + return gst_pad_query_default(pad, parent, query); +} + + + + + +static gboolean plugin_init(GstPlugin* plugin) +{ + GST_DEBUG_CATEGORY_INIT(gst_krkr_debug, "plugin", 0, "krkr plugin"); + return TRUE; + return GST_ELEMENT_REGISTER(krkr_demux, plugin) && GST_ELEMENT_REGISTER(krkr_video, plugin); +} + +#define PACKAGE "krkrwine" +GST_PLUGIN_DEFINE( + // overriding the version like this makes me a Bad Person(tm), but Proton 8.0 is on 1.18.5, so I need to stay behind + 1, // GST_VERSION_MAJOR, + 18, // GST_VERSION_MINOR, + G_PASTE(krkr_, PLUGINARCH), + "krkr PL_MPEG (MPEG-1 decoder) wrapper for GStreamer", + plugin_init, + "1.0", + "LGPL", + "gstkrkr", + "https://walrus.se/" +) diff --git a/x/gstkrkr-video-audio-fakewma.c b/x/gstkrkr-video-audio-fakewma.c new file mode 100644 index 0000000..8e6b2c8 --- /dev/null +++ b/x/gstkrkr-video-audio-fakewma.c @@ -0,0 +1,648 @@ +// SPDX-License-Identifier: LGPL-2.0-or-later + +#include +#include +#define PL_MPEG_IMPLEMENTATION +#include "pl_mpeg.h" +#include "mpeg_packet.h" + +/** + * SECTION:element-plugin + * + * FIXME:Describe plugin here. + * + * + * Example launch line + * |[ + * gst-launch-1.0 filesrc location=video.mpg ! mpegpsdemux name=demux ! mpegvideoparse ! avdec_mpeg2video ! queue ! autovideosink \ + * demux. ! mpegaudioparse ! avdec_mp2float ! audioconvert ! queue ! autoaudiosink + * gst-launch-1.0 filesrc location=video.mpg ! mpegpsdemux name=demux ! krkr_mpegvideo ! queue ! autovideosink \ + * demux. ! krkr_mpegaudio ! audioconvert ! queue ! autoaudiosink + * ]| + * + */ + +GST_DEBUG_CATEGORY_STATIC(gst_krkr_debug); +#define GST_CAT_DEFAULT gst_krkr_debug + +// in GStreamer, decoding a mpeg video requires three elements: mpegpsdemux ! mpegvideoparse ! avdec_mpeg2video +// in pl_mpeg, the latter two are merged to one object +// in DirectShow, the FORMER two are merged to one object +// (similar for audio, the elements are named mpegpsdemux ! mpegaudioparse ! avdec_mp2float) +// luckily, the parser elements just take byte sequences and return packets, consisting of the same bytes +// (but with packet boundaries at significant locations); stacking two mpegvideoparse elements is useless but harmless + +// rank rules: Must be higher than media-converter, and lower than every relevant official GStreamer filter. +// The relevant filters are +// - protonaudioconverter MARGINAL audio/x-wma +// - protonaudioconverterbin MARGINAL+1 audio/x-wma +// - protonvideoconverter MARGINAL video/x-ms-asf, video/x-msvideo, video/mpeg, video/quicktime +// - mpegpsdemux PRIMARY video/mpeg(systemstream=true) +// - mpegvideoparse PRIMARY+1 video/mpeg(systemstream=false) +// - avdec_mpeg2video PRIMARY video/mpeg(mpegversion=[1,2], systemstream=false, parsed=false) +// - mpegaudioparse PRIMARY+2 audio/mpeg(mpegversion=1, layer=2) +// - avdec_mp2float MARGINAL audio/mpeg(mpegversion=1, layer=2, parsed=true) +// - asfdemux SECONDARY video/x-ms-asf +// - avdec_wma* MARGINAL audio/x-wma +// - avdec_wmv* MARGINAL video/x-wmv + +// Most of those elements are present in Proton, I only need to reimplement avdec_mpeg2video and avdec_mp2float +// (and I don't need mpegvideoparse and mpegaudioparse, though I don't mind if they exist). +// I need to go above protonvideoconverter, which means my mpeg2video should use MARGINAL+1. +// For mp2float, the real decoder is MARGINAL, so I need to go below that. Luckily, media-converter doesn't want audio/mpeg. +// The problem is audio/x-wma. I don't need to implement any of them myself, but protonaudioconverterbin is above avdec_wma*. +// I can't change either of them, and I don't want to remove any files from Proton, so I'll have to do something ugly: +// A fake element whose only job is to create a real WMA decoder, with rank MARGINAL+2. + +// Sources are mostly gst-inspect-1.0; some source code is available at +// +// +// +// +// + +#define VIDEO_RANK GST_RANK_MARGINAL+1 +#define AUDIO_RANK GST_RANK_MARGINAL-1 +#define FAKEWMA_RANK GST_RANK_MARGINAL+2 + +//#define VIDEO_RANK GST_RANK_PRIMARY+2 +//#define AUDIO_RANK GST_RANK_PRIMARY+2 +//#define FAKEWMA_RANK GST_RANK_PRIMARY+2 + +static void print_event(const char * pad_name, GstEvent* event) +{ + return; + fprintf(stderr, "gstkrkr: Received %s event on %s\n", GST_EVENT_TYPE_NAME(event), pad_name); + return; + gst_print("gstkrkr: Received %s event on %s: ", GST_EVENT_TYPE_NAME(event), pad_name); + + switch (GST_EVENT_TYPE(event)) + { + case GST_EVENT_CAPS: + { + GstCaps* caps; + gst_event_parse_caps(event, &caps); + gst_print("%" GST_PTR_FORMAT, caps); + break; + } + case GST_EVENT_SEGMENT: + { + const GstSegment * segment; + gst_event_parse_segment(event, &segment); + gst_print("%lu/%lu\n", (unsigned long)segment->position, (unsigned long)segment->duration); + break; + } + case GST_EVENT_TAG: + { + GstTagList* taglist; + gst_event_parse_tag(event, &taglist); + gst_print("%" GST_PTR_FORMAT, taglist); + break; + } + default: + gst_print("(unknown type)"); + } +} + +static void print_query(const char * pad_name, GstQuery* query) +{ + return; + fprintf(stderr, "gstkrkr: Received %s query on %s\n", GST_QUERY_TYPE_NAME(query), pad_name); +} + +// same as gst_buffer_new_memdup, except that function is new in GStreamer 1.20; Proton's is version 1.18.5 +static GstBuffer* wrap_bytes_to_gst(const uint8_t * buf, size_t size) +{ + GBytes* by = g_bytes_new(buf, size); + GstBuffer* ret = gst_buffer_new_wrapped_bytes(by); + g_bytes_unref(by); + return ret; +} + + + +#define GST_TYPE_PLMPEG_VIDEO (gst_krkr_video_get_type()) +G_DECLARE_FINAL_TYPE(GstKrkrPlMpegVideo, gst_krkr_video, GST, KRKRPLMPEG_VIDEO, GstElement) + +struct _GstKrkrPlMpegVideo +{ + GstElement element; + + GstPad* sinkpad; + GstPad* srcpad; + + int width; + int height; + + plm_buffer_t* buf; + plm_video_t* decode; +}; + +static GstStaticPadTemplate decodevideo_sink_factory = GST_STATIC_PAD_TEMPLATE( + "sink", + GST_PAD_SINK, + GST_PAD_ALWAYS, + GST_STATIC_CAPS("video/mpeg, mpegversion=(int)1, systemstream=(boolean)false") + ); + +static GstStaticPadTemplate decodevideo_src_factory = GST_STATIC_PAD_TEMPLATE( + "src", + GST_PAD_SRC, + GST_PAD_ALWAYS, + GST_STATIC_CAPS("video/x-raw, format=(string)YV12") + ); + +G_DEFINE_TYPE(GstKrkrPlMpegVideo, gst_krkr_video, GST_TYPE_ELEMENT); + +GST_ELEMENT_REGISTER_DECLARE(krkr_video); +GST_ELEMENT_REGISTER_DEFINE(krkr_video, "krkr_mpegvideo", VIDEO_RANK, GST_TYPE_PLMPEG_VIDEO); + +static void gst_krkr_video_finalize(GObject* object); + +static gboolean gst_krkr_video_sink_event(GstPad* pad, GstObject* parent, GstEvent* event); +static GstFlowReturn gst_krkr_video_sink_chain(GstPad* pad, GstObject* parent, GstBuffer* buf); +static gboolean gst_krkr_video_src_event(GstPad* pad, GstObject* parent, GstEvent* event); +static gboolean gst_krkr_video_src_query(GstPad* pad, GstObject* parent, GstQuery* query); + +static void gst_krkr_video_class_init(GstKrkrPlMpegVideoClass* klass) +{ + GObjectClass* gobject_class; + GstElementClass* gstelement_class; + + gobject_class = (GObjectClass*)klass; + gstelement_class = (GstElementClass*)klass; + + gobject_class->finalize = gst_krkr_video_finalize; + + gst_element_class_set_details_simple(gstelement_class, + "krkr_mpegvideo", + "Decoder/Video", + "MPEG-1 video decoder", + "Sir Walrus sir@walrus.se"); + + gst_element_class_add_pad_template(gstelement_class, gst_static_pad_template_get(&decodevideo_sink_factory)); + gst_element_class_add_pad_template(gstelement_class, gst_static_pad_template_get(&decodevideo_src_factory)); +} + +static void gst_krkr_video_init(GstKrkrPlMpegVideo* filter) +{ + filter->sinkpad = gst_pad_new_from_static_template(&decodevideo_sink_factory, "sink"); + gst_pad_set_event_function(filter->sinkpad, GST_DEBUG_FUNCPTR(gst_krkr_video_sink_event)); + gst_pad_set_chain_function(filter->sinkpad, GST_DEBUG_FUNCPTR(gst_krkr_video_sink_chain)); + GST_OBJECT_FLAG_SET(filter->sinkpad, GST_PAD_FLAG_NEED_PARENT); + gst_element_add_pad(GST_ELEMENT(filter), filter->sinkpad); + + filter->srcpad = gst_pad_new_from_static_template(&decodevideo_src_factory, "src"); + gst_pad_set_event_function(filter->srcpad, GST_DEBUG_FUNCPTR(gst_krkr_video_src_event)); + gst_pad_set_query_function(filter->srcpad, GST_DEBUG_FUNCPTR(gst_krkr_video_src_query)); + GST_OBJECT_FLAG_SET(filter->srcpad, GST_PAD_FLAG_NEED_PARENT); + gst_element_add_pad(GST_ELEMENT(filter), filter->srcpad); + + filter->buf = plm_buffer_create_with_capacity(PLM_BUFFER_DEFAULT_SIZE); + filter->decode = plm_video_create_with_buffer(filter->buf, false); + + //fprintf(stderr, "gstkrkr: Created a decoder\n"); +} + +static void gst_krkr_video_finalize(GObject* object) +{ + GstKrkrPlMpegVideo* filter = GST_KRKRPLMPEG_VIDEO(object); + + plm_buffer_destroy(filter->buf); + plm_video_destroy(filter->decode); + + G_OBJECT_CLASS(gst_krkr_video_parent_class)->finalize(object); +} + +static gboolean gst_krkr_video_sink_event(GstPad* pad, GstObject* parent, GstEvent* event) +{ + print_event("video sink", event); + + GstKrkrPlMpegVideo* filter = GST_KRKRPLMPEG_VIDEO(parent); + + switch (GST_EVENT_TYPE(event)) + { + case GST_EVENT_CAPS: + { + GstCaps* caps_in; + gst_event_parse_caps(event, &caps_in); + GstStructure* struc = gst_caps_get_structure(caps_in, 0); + + // expected input caps are + // video/mpeg, mpegversion=(int)1, systemstream=(boolean)false, parsed=(boolean)true, width=(int)640, height=(int)360, + // framerate=(fraction)30000/1001, pixel-aspect-ratio=(fraction)1/1, codec_data=(buffer)000001b328016814ffffe018 + + // output is + // video/x-raw, format=(string)YV12, width=(int)640, height=(int)360, interlace-mode=(string)progressive, + // pixel-aspect-ratio=(fraction)1/1, chroma-site=(string)jpeg, colorimetry=(string)2:0:0:0, framerate=(fraction)30000/1001 + gst_structure_get_int(struc, "width", &filter->width); + gst_structure_get_int(struc, "height", &filter->height); + int framerate_n; + int framerate_d; + gst_structure_get_fraction(struc, "framerate", &framerate_n, &framerate_d); + + GstCaps* caps_out = gst_caps_new_simple("video/x-raw", + "format", G_TYPE_STRING, "YV12", + "width", G_TYPE_INT, filter->width, + "height", G_TYPE_INT, filter->height, + "framerate", GST_TYPE_FRACTION, framerate_n, framerate_d, + NULL); + gst_pad_push_event(filter->srcpad, gst_event_new_caps(caps_out)); + return TRUE; + } + default: + return gst_pad_event_default(pad, parent, event); + } +} + +static GstFlowReturn gst_krkr_video_sink_chain(GstPad* pad, GstObject* parent, GstBuffer* buf) +{ + GstKrkrPlMpegVideo* filter = GST_KRKRPLMPEG_VIDEO(parent); +//fprintf(stderr, "gstkrkr: decode %lu bytes, send %d\n", gst_buffer_get_size(buf), filter->should_send_frames); + for (size_t n=0;nbuf, meminf.data, meminf.size); + gst_memory_unref(mem); + } + + while (true) + { + plm_frame_t* frame = plm_video_decode(filter->decode); +//fprintf(stderr, "gstkrkr: decoded to %p\n", frame); + if (!frame) + break; + + size_t buflen = frame->width*frame->height*12/8; + uint8_t* ptr = g_malloc(buflen); + GstBuffer* buf = gst_buffer_new_wrapped(ptr, buflen); + for (int y=0;yheight;y++) + { + memcpy(ptr, frame->y.data + frame->y.width*y, frame->width); + ptr += frame->width; + } + for (int y=0;yheight/2;y++) + { + memcpy(ptr, frame->cr.data + frame->cr.width*y, frame->width/2); + ptr += frame->width/2; + } + for (int y=0;yheight/2;y++) + { + memcpy(ptr, frame->cb.data + frame->cb.width*y, frame->width/2); + ptr += frame->width/2; + } + + buf->pts = frame->time * 1000000000; + buf->dts = frame->time * 1000000000; + buf->duration = 1000000000 / plm_video_get_framerate(filter->decode); +//fprintf(stderr, "gstkrkr: send frame, %lu bytes\n", gst_buffer_get_size(buf)); + gst_pad_push(filter->srcpad, buf); + } + + return GST_FLOW_OK; +} + +static gboolean gst_krkr_video_src_event(GstPad* pad, GstObject* parent, GstEvent* event) +{ + print_event("video source", event); + + //GstKrkrPlMpegVideo* filter = GST_PLMPEG_VIDEO(parent); + + switch (GST_EVENT_TYPE(event)) { + case GST_EVENT_CAPS: + return TRUE; + default: + break; + } + return gst_pad_event_default(pad, parent, event); +} + +static gboolean gst_krkr_video_src_query(GstPad* pad, GstObject* parent, GstQuery* query) +{ + print_query("video src", query); + return gst_pad_query_default(pad, parent, query); +} + + + + + +#define GST_TYPE_PLMPEG_AUDIO (gst_krkr_audio_get_type()) +G_DECLARE_FINAL_TYPE(GstKrkrPlMpegAudio, gst_krkr_audio, GST, KRKRPLMPEG_AUDIO, GstElement) + +struct _GstKrkrPlMpegAudio +{ + GstElement element; + + GstPad* sinkpad; + GstPad* srcpad; + + plm_buffer_t* buf; + plm_audio_t* decode; +}; + +static GstStaticPadTemplate decodeaudio_sink_factory = GST_STATIC_PAD_TEMPLATE( + "sink", + GST_PAD_SINK, + GST_PAD_ALWAYS, + GST_STATIC_CAPS("audio/mpeg, mpegversion=(int)1, layer=(int)2") + ); + +static GstStaticPadTemplate decodeaudio_src_factory = GST_STATIC_PAD_TEMPLATE( + "src", + GST_PAD_SRC, + GST_PAD_ALWAYS, + GST_STATIC_CAPS("audio/x-raw, format=(string)F32LE") + ); + +G_DEFINE_TYPE(GstKrkrPlMpegAudio, gst_krkr_audio, GST_TYPE_ELEMENT); + +GST_ELEMENT_REGISTER_DECLARE(krkr_audio); +GST_ELEMENT_REGISTER_DEFINE(krkr_audio, "krkr_mpegaudio", AUDIO_RANK, GST_TYPE_PLMPEG_AUDIO); + +static void gst_krkr_audio_finalize(GObject* object); + +static gboolean gst_krkr_audio_sink_event(GstPad* pad, GstObject* parent, GstEvent* event); +static GstFlowReturn gst_krkr_audio_sink_chain(GstPad* pad, GstObject* parent, GstBuffer* buf); +static gboolean gst_krkr_audio_src_event(GstPad* pad, GstObject* parent, GstEvent* event); +static gboolean gst_krkr_audio_src_query(GstPad* pad, GstObject* parent, GstQuery* query); + +static void gst_krkr_audio_class_init(GstKrkrPlMpegAudioClass* klass) +{ + GObjectClass* gobject_class; + GstElementClass* gstelement_class; + + gobject_class = (GObjectClass*)klass; + gstelement_class = (GstElementClass*)klass; + + gobject_class->finalize = gst_krkr_audio_finalize; + + gst_element_class_set_details_simple(gstelement_class, + "krkr_mpegaudio", + "Decoder/Audio", + "MPEG-1 layer 2 audio decoder", + "Sir Walrus sir@walrus.se"); + + gst_element_class_add_pad_template(gstelement_class, gst_static_pad_template_get(&decodeaudio_sink_factory)); + gst_element_class_add_pad_template(gstelement_class, gst_static_pad_template_get(&decodeaudio_src_factory)); +} + +static void gst_krkr_audio_init(GstKrkrPlMpegAudio* filter) +{ + filter->sinkpad = gst_pad_new_from_static_template(&decodeaudio_sink_factory, "sink"); + gst_pad_set_event_function(filter->sinkpad, GST_DEBUG_FUNCPTR(gst_krkr_audio_sink_event)); + gst_pad_set_chain_function(filter->sinkpad, GST_DEBUG_FUNCPTR(gst_krkr_audio_sink_chain)); + GST_OBJECT_FLAG_SET(filter->sinkpad, GST_PAD_FLAG_NEED_PARENT); + gst_element_add_pad(GST_ELEMENT(filter), filter->sinkpad); + + filter->srcpad = gst_pad_new_from_static_template(&decodeaudio_src_factory, "src"); + gst_pad_set_event_function(filter->srcpad, GST_DEBUG_FUNCPTR(gst_krkr_audio_src_event)); + gst_pad_set_query_function(filter->srcpad, GST_DEBUG_FUNCPTR(gst_krkr_audio_src_query)); + GST_OBJECT_FLAG_SET(filter->srcpad, GST_PAD_FLAG_NEED_PARENT); + gst_element_add_pad(GST_ELEMENT(filter), filter->srcpad); + + filter->buf = plm_buffer_create_with_capacity(PLM_BUFFER_DEFAULT_SIZE); + filter->decode = plm_audio_create_with_buffer(filter->buf, false); + + //fprintf(stderr, "gstkrkr: Created a decoder\n"); +} + +static void gst_krkr_audio_finalize(GObject* object) +{ + GstKrkrPlMpegAudio* filter = GST_KRKRPLMPEG_AUDIO(object); + + plm_buffer_destroy(filter->buf); + plm_audio_destroy(filter->decode); + + G_OBJECT_CLASS(gst_krkr_audio_parent_class)->finalize(object); +} + +static gboolean gst_krkr_audio_sink_event(GstPad* pad, GstObject* parent, GstEvent* event) +{ + print_event("audio sink", event); + + GstKrkrPlMpegAudio* filter = GST_KRKRPLMPEG_AUDIO(parent); + + switch (GST_EVENT_TYPE(event)) + { + case GST_EVENT_CAPS: + { + GstCaps* caps_in; + gst_event_parse_caps(event, &caps_in); + GstStructure* struc = gst_caps_get_structure(caps_in, 0); + + // expected input caps are + // audio/mpeg, mpegversion=(int)1, mpegaudioversion=(int)1, layer=(int)2, rate=(int)44100, channels=(int)2, parsed=(boolean)true + + // output is + // audio/x-raw, format=(string)F32LE, rate=(int)44100, channels=(int)2, layout=(string)interleaved + int samplerate; + gst_structure_get_int(struc, "rate", &samplerate); + + GstCaps* caps_out = gst_caps_new_simple("audio/x-raw", + "format", G_TYPE_STRING, "F32LE", // actually native endian, but everything is LE these days + "rate", G_TYPE_INT, samplerate, + "channels", G_TYPE_INT, 2, // it seems that PL_MPEG always emits dual-channel samples, even if the actual file is mono + "layout", G_TYPE_STRING, "interleaved", + NULL); + gst_pad_push_event(filter->srcpad, gst_event_new_caps(caps_out)); + return TRUE; + } + default: + return gst_pad_event_default(pad, parent, event); + } +} + +static GstFlowReturn gst_krkr_audio_sink_chain(GstPad* pad, GstObject* parent, GstBuffer* buf) +{ + GstKrkrPlMpegAudio* filter = GST_KRKRPLMPEG_AUDIO(parent); +//fprintf(stderr, "gstkrkr: decode %lu bytes, send %d\n", gst_buffer_get_size(buf), filter->should_send_frames); + for (size_t n=0;nbuf, meminf.data, meminf.size); + gst_memory_unref(mem); + } + + while (true) + { + plm_samples_t* samp = plm_audio_decode(filter->decode); +//fprintf(stderr, "gstkrkr: decoded to %p\n", samp); + if (!samp) + break; + + size_t buflen = samp->count * sizeof(float) * 2; + GstBuffer* buf = wrap_bytes_to_gst((uint8_t*)samp->interleaved, buflen); + + buf->pts = samp->time * 1000000000; + buf->dts = samp->time * 1000000000; + buf->duration = 1000000000 / plm_audio_get_samplerate(filter->decode); +//fprintf(stderr, "gstkrkr: send frame, %lu bytes\n", gst_buffer_get_size(buf)); + gst_pad_push(filter->srcpad, buf); + } + + return GST_FLOW_OK; +} + +static gboolean gst_krkr_audio_src_event(GstPad* pad, GstObject* parent, GstEvent* event) +{ + print_event("audio source", event); + + //GstKrkrPlMpegAudio* filter = GST_PLMPEG_AUDIO(parent); + + switch (GST_EVENT_TYPE(event)) { + case GST_EVENT_CAPS: + return TRUE; + default: + break; + } + return gst_pad_event_default(pad, parent, event); +} + +static gboolean gst_krkr_audio_src_query(GstPad* pad, GstObject* parent, GstQuery* query) +{ + print_query("audio src", query); + return gst_pad_query_default(pad, parent, query); +} + + + + + +#define GST_TYPE_FAKEWMA (gst_krkr_fakewma_get_type()) +G_DECLARE_FINAL_TYPE(GstKrkrFakeWma, gst_krkr_fakewma, GST, KRKR_FAKEWMA, GstBin) + +struct _GstKrkrFakeWma +{ + GstBin bin; + + GstPad* sinkpad; + GstPad* srcpad; + + GstElement* decodebin; +}; + +static GstStaticPadTemplate fakewma_sink_factory = GST_STATIC_PAD_TEMPLATE( + "sink", + GST_PAD_SINK, + GST_PAD_ALWAYS, + GST_STATIC_CAPS("audio/x-wma") + ); + +static GstStaticPadTemplate fakewma_src_factory = GST_STATIC_PAD_TEMPLATE( + "src", + GST_PAD_SRC, + GST_PAD_ALWAYS, + GST_STATIC_CAPS("audio/x-raw") + ); + +G_DEFINE_TYPE(GstKrkrFakeWma, gst_krkr_fakewma, GST_TYPE_BIN); + +GST_ELEMENT_REGISTER_DECLARE(krkr_fakewma); +GST_ELEMENT_REGISTER_DEFINE(krkr_fakewma, "krkr_fakewma", FAKEWMA_RANK, GST_TYPE_FAKEWMA); + +static void gst_krkr_fakewma_finalize(GObject* object); + +static void gst_krkr_fakewma_class_init(GstKrkrFakeWmaClass* klass) +{ + GObjectClass* gobject_class; + GstElementClass* gstelement_class; + + gobject_class = (GObjectClass*)klass; + gstelement_class = (GstElementClass*)klass; + + gst_element_class_set_details_simple(gstelement_class, + "krkr_fakewma", + "Decoder/Audio", + "Fake WMA decoder, loads another one while avoiding protonaudioconverter", + "Sir Walrus sir@walrus.se"); + + gst_element_class_add_pad_template(gstelement_class, gst_static_pad_template_get(&fakewma_sink_factory)); + gst_element_class_add_pad_template(gstelement_class, gst_static_pad_template_get(&fakewma_src_factory)); +} + +static void valuearray_insert_object(GValueArray* arr, size_t pos, void* object) +{ + GValue val = {}; + g_value_init(&val, G_TYPE_OBJECT); + g_value_set_object(&val, object); + g_value_array_insert(arr, pos, &val); + g_value_unset(&val); +} + +static GValueArray* gst_krkr_fakewma_decodebin_sort(GstElement* bin, GstPad* pad, GstCaps* caps, GValueArray* factories, void* udata) +{ + // keeps throwing trash about "g_value_array_new is deprecated: Use 'GArray' instead" + // don't care, GStreamer uses GValueArray so I need this + GValueArray* ret = g_value_array_new(factories->n_values); + size_t n_real = 0; + size_t n_mediaconverter = 0; + for (size_t n=0;nn_values;n++) + { + GstElementFactory* factory = g_value_get_object(&factories->values[n]); + + // discard ourselves, that's just recursion + if (!strcmp(gst_plugin_feature_get_name(factory), "krkr_fakewma")) + continue; + + // move mediaconverter to the end (it's better than nothing...) + bool is_mediaconverter = (!strncmp(gst_plugin_feature_get_name(factory), "proton", strlen("proton"))); + + if (is_mediaconverter) + valuearray_insert_object(ret, n_real++, factory); + else + valuearray_insert_object(ret, n_real + n_mediaconverter++, factory); + } + return ret; +} + +static void gst_krkr_fakewma_decodebin_pad_added(GstElement* self, GstPad* new_pad, void* user_data) +{ + GstKrkrFakeWma* filter = GST_KRKR_FAKEWMA(user_data); + + gst_ghost_pad_set_target(GST_GHOST_PAD(filter->srcpad), new_pad); +} + +static void gst_krkr_fakewma_init(GstKrkrFakeWma* filter) +{ + //fprintf(stderr, "gstkrkr: Created a fakewma\n"); + + filter->decodebin = gst_element_factory_make("decodebin", "decodebin"); + gst_bin_add(GST_BIN(filter), filter->decodebin); + g_signal_connect(filter->decodebin, "autoplug-sort", G_CALLBACK(gst_krkr_fakewma_decodebin_sort), filter); + g_signal_connect(filter->decodebin, "pad-added", G_CALLBACK(gst_krkr_fakewma_decodebin_pad_added), filter); + + filter->sinkpad = gst_ghost_pad_new("sink", gst_element_get_static_pad(filter->decodebin, "sink")); + gst_element_add_pad(GST_ELEMENT(filter), filter->sinkpad); + + filter->srcpad = gst_ghost_pad_new_no_target_from_template("src", gst_static_pad_template_get(&fakewma_src_factory)); + gst_element_add_pad(GST_ELEMENT(filter), filter->srcpad); +} + + + +static gboolean plugin_init(GstPlugin* plugin) +{ + GST_DEBUG_CATEGORY_INIT(gst_krkr_debug, "plugin", 0, "krkrwine plugin"); + return GST_ELEMENT_REGISTER(krkr_video, plugin) && + //GST_ELEMENT_REGISTER(krkr_audio, plugin) && + GST_ELEMENT_REGISTER(krkr_fakewma, plugin); +} + +#define PACKAGE "krkrwine" +GST_PLUGIN_DEFINE( + // overriding the version like this makes me a Bad Person(tm), but Proton 8.0 is on 1.18.5, so I need to stay behind + 1, // GST_VERSION_MAJOR, + 18, // GST_VERSION_MINOR, + G_PASTE(krkr_, PLUGINARCH), + "krkrwine module for GStreamer", + plugin_init, + "1.0", + "LGPL", + "gstkrkr", + "https://walrus.se/" +) diff --git a/x/homemade-source-and-sink.cpp b/x/homemade-source-and-sink.cpp new file mode 100644 index 0000000..c34f045 --- /dev/null +++ b/x/homemade-source-and-sink.cpp @@ -0,0 +1,829 @@ +// SPDX-License-Identifier: LGPL-2.0-or-later + +// can be compiled with +// wine g++ -std=c++20 nanodecode3.cpp -lole32 -fno-exceptions -fno-rtti +// (where g++ refers to, for example, https://winlibs.com/ ) + +// the resulting a.bin can be converted to png with imagemagick +// convert -depth 8 -size 640x360 bgr:a.bin -flip a.png +// (size may be different if you're using a video other than data_video_sample_640x360.mpeg) + +#define INITGUID +#define STRSAFE_NO_DEPRECATE +#include +#include +#include +#include +#include + +static const GUID GUID_NULL = {}; // not defined in my headers, how lovely +DEFINE_GUID(CLSID_decodebin_parser, 0xf9d8d64e, 0xa144, 0x47dc, 0x8e, 0xe0, 0xf5, 0x34, 0x98, 0x37, 0x2c, 0x29); + +static char* guid_to_str(const GUID& guid) +{ + static char buf[8][64]; + static int n = 0; + char* ret = buf[n++%8]; + sprintf(ret, "{%08lX-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}", guid.Data1, guid.Data2, guid.Data3, guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3], guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]); + return ret; +} + + +template class CComPtr { + void assign(T* ptr) + { + p = ptr; + } + void release() + { + if (p) + p->Release(); + p = nullptr; + } +public: + T* p; + + CComPtr() { p = nullptr; } + ~CComPtr() { release(); } + + CComPtr& operator=(T* ptr) + { + release(); + assign(ptr); + return *this; + } + T** operator&() + { + release(); + return &p; + } + T* operator->() { return p; } + operator T*() { return p; } + + HRESULT CoCreateInstance(REFCLSID rclsid, LPUNKNOWN pUnkOuter, DWORD dwClsContext) + { + release(); + return ::CoCreateInstance(rclsid, pUnkOuter, dwClsContext, IID_PPV_ARGS(&p)); + } + template + HRESULT QueryInterface(T2** other) + { + return p->QueryInterface(IID_PPV_ARGS(other)); + } +}; + +template +class com_base_embedded : public Tis... { +private: + template T first_helper(); + template bool QueryInterfaceSingle(REFIID riid, void** ppvObject) + { + if (riid == __uuidof(Ti)) + { + Ti* ret = this; + ret->AddRef(); + *ppvObject = (void*)ret; + return true; + } + else return false; + } +public: + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override + { +printf("plm QI %s\n", guid_to_str(riid)); + *ppvObject = nullptr; + if (riid == __uuidof(IUnknown)) + { + IUnknown* ret = (decltype(first_helper())*)this; + ret->AddRef(); + *ppvObject = (void*)ret; + return S_OK; + } + return (QueryInterfaceSingle(riid, ppvObject) || ...) ? S_OK : E_NOINTERFACE; + } +}; + +template +class com_base : public com_base_embedded { + uint32_t refcount = 1; +public: + ULONG STDMETHODCALLTYPE AddRef() override { return ++refcount; } + ULONG STDMETHODCALLTYPE Release() override + { + uint32_t new_refcount = --refcount; + if (!new_refcount) + delete this; + return new_refcount; + } +}; + +template +Tinner com_enum_helper(HRESULT STDMETHODCALLTYPE (Touter::*)(ULONG, Tinner**, ULONG*)); + +template +class com_enum : public com_base { + using Tret = decltype(com_enum_helper(&Timpl::Next)); + + Tret** items; + size_t pos = 0; + size_t len; + + com_enum(Tret** items, size_t pos, size_t len) : items(items), pos(pos), len(len) {} +public: + com_enum(Tret** items, size_t len) : items(items), len(len) {} + + HRESULT STDMETHODCALLTYPE Clone(Timpl** ppEnum) override + { + *ppEnum = new com_enum(items, pos, len); + return S_OK; + } + HRESULT STDMETHODCALLTYPE Next(ULONG celt, Tret** rgelt, ULONG* pceltFetched) override + { + size_t remaining = len - pos; + size_t ret = celt; + if (ret > remaining) + ret = remaining; + for (size_t n=0;nAddRef(); + } + if (pceltFetched) + *pceltFetched = ret; + if (remaining >= celt) + return S_OK; + else + return S_FALSE; + } + HRESULT STDMETHODCALLTYPE Reset() override + { + pos = 0; + return S_OK; + } + HRESULT STDMETHODCALLTYPE Skip(ULONG celt) override + { + size_t remaining = len - pos; + if (remaining >= celt) + { + pos += celt; + return S_OK; + } + else + { + pos = len; + return S_FALSE; + } + } +}; + + +// inspired by the Linux kernel macro, but using a member pointer looks cleaner; non-expressions (like member names) in macros look wrong +template Tc* container_of(Ti* ptr, Ti Tc:: * memb) +{ + // https://wg21.link/P0908 proposes a better implementation, but it was forgotten and not accepted + Tc* fake_object = (Tc*)0x12345678; // doing math on a fake pointer is UB, but good luck proving it's bogus + size_t offset = (uintptr_t)&(fake_object->*memb) - (uintptr_t)fake_object; + return (Tc*)((uint8_t*)ptr - offset); +} +template const Tc* container_of(const Ti* ptr, Ti Tc:: * memb) +{ + return container_of((Ti*)ptr, memb); +} +template auto container_of(Ti* ptr) { return container_of(ptr, memb); } + + +HRESULT qi_release(IUnknown* obj, REFIID riid, void** ppvObj) +{ + HRESULT hr = obj->QueryInterface(riid, ppvObj); + obj->Release(); + return hr; +} + + +class scoped_lock { + SRWLOCK* lock; +public: + scoped_lock(SRWLOCK* lock) : lock(lock) { AcquireSRWLockExclusive(lock); } + ~scoped_lock() { ReleaseSRWLockExclusive(lock); } +}; +class scoped_unlock { + SRWLOCK* lock; +public: + scoped_unlock(SRWLOCK* lock) : lock(lock) { ReleaseSRWLockExclusive(lock); } + ~scoped_unlock() { AcquireSRWLockExclusive(lock); } +}; + + +template +class base_filter : public com_base { +public: + void debug(const char * fmt, ...) + { + char buf[1024]; + + va_list args; + va_start(args, fmt); + vsnprintf(buf, 1024, fmt, args); + va_end(args); + + fprintf(stdout, "%lu %s %s\n", GetCurrentThreadId(), typeid(Touter).name(), buf); + fflush(stdout); + } + + Touter* parent() + { + return (Touter*)this; + } + + // several pointers in here aren't CComPtr, due to reference cycles + IFilterGraph* graph = nullptr; + + FILTER_STATE state = State_Stopped; + + HRESULT STDMETHODCALLTYPE GetClassID(CLSID* pClassID) override + { + debug("IPersist GetClassID"); + *pClassID = {}; + return E_UNEXPECTED; + } + + HRESULT STDMETHODCALLTYPE GetState(DWORD dwMilliSecsTimeout, FILTER_STATE* State) override + { + debug("IMediaFilter GetState %lx", state); + *State = state; + return S_OK; + } + HRESULT STDMETHODCALLTYPE GetSyncSource(IReferenceClock** pClock) override + { + debug("IMediaFilter GetSyncSource"); + *pClock = nullptr; + return S_OK; + } + HRESULT STDMETHODCALLTYPE Pause() override + { + debug("IMediaFilter Pause"); + state = State_Paused; + return S_OK; + } + HRESULT STDMETHODCALLTYPE Run(REFERENCE_TIME tStart) override + { + debug("IMediaFilter Run"); + state = State_Running; + return S_OK; + } + HRESULT STDMETHODCALLTYPE SetSyncSource(IReferenceClock* pClock) override + { + debug("IMediaFilter SetSyncSource"); + return S_OK; + } + HRESULT STDMETHODCALLTYPE Stop() override + { + debug("IMediaFilter Stop"); + return S_OK; + } + + IEnumPins* enum_pins() requires requires { sizeof(parent()->pins); } + { + return new com_enum(parent()->pins, sizeof(parent()->pins) / sizeof(parent()->pins[0])); + } + HRESULT STDMETHODCALLTYPE EnumPins(IEnumPins** ppEnum) override + { + debug("IBaseFilter EnumPins"); + *ppEnum = parent()->enum_pins(); + return S_OK; + } + HRESULT STDMETHODCALLTYPE FindPin(LPCWSTR Id, IPin** ppPin) override + { + debug("IBaseFilter FindPin"); + *ppPin = nullptr; + return VFW_E_NOT_FOUND; + } + HRESULT STDMETHODCALLTYPE JoinFilterGraph(IFilterGraph* pGraph, LPCWSTR pName) override + { + debug("IBaseFilter JoinFilterGraph"); + graph = pGraph; + return S_OK; + } + HRESULT STDMETHODCALLTYPE QueryFilterInfo(FILTER_INFO* pInfo) override + { + debug("IBaseFilter QueryFilterInfo"); + wcscpy(pInfo->achName, L"my object"); + pInfo->pGraph = graph; + graph->AddRef(); + return S_OK; + } + HRESULT STDMETHODCALLTYPE QueryVendorInfo(LPWSTR* pVendorInfo) override + { + debug("IBaseFilter QueryVendorInfo"); + return E_NOTIMPL; + } +}; + +template +class base_pin : public Tbase { +public: + IPin* peer = nullptr; + + void debug(const char * fmt, ...) + { + char buf[1024]; + + va_list args; + va_start(args, fmt); + vsnprintf(buf, 1024, fmt, args); + va_end(args); + + fprintf(stdout, "%lu %s %s\n", GetCurrentThreadId(), typeid(Touter).name(), buf); + } + + Touter* parent() + { + return (Touter*)this; + } + + ULONG STDMETHODCALLTYPE AddRef() override { return parent()->parent()->AddRef(); } + ULONG STDMETHODCALLTYPE Release() override { return parent()->parent()->Release(); } + + HRESULT STDMETHODCALLTYPE BeginFlush() override + { + debug("IPin BeginFlush"); + if (is_output) + return E_UNEXPECTED; + return E_OUTOFMEMORY; + } + HRESULT STDMETHODCALLTYPE Connect(IPin* pReceivePin, const AM_MEDIA_TYPE * pmt) override + { + debug("IPin Connect %p", pmt); + if constexpr (is_output) + { + //debug("IPin Connect %s %s %s", guid_to_str(pmt->majortype), guid_to_str(pmt->subtype), guid_to_str(pmt->formattype)); + if (parent()->connect_output(pReceivePin)) + { + puts("OUTPUT PIN CONNECTED"); + peer = pReceivePin; + return S_OK; + } + return VFW_E_NO_ACCEPTABLE_TYPES; + } + else return E_UNEXPECTED; + } + HRESULT STDMETHODCALLTYPE ConnectedTo(IPin** pPin) override + { + debug("IPin ConnectedTo"); + if (!peer) + return VFW_E_NOT_CONNECTED; + *pPin = peer; + peer->AddRef(); + return S_OK; + } + HRESULT STDMETHODCALLTYPE ConnectionMediaType(AM_MEDIA_TYPE* pmt) override + { + debug("IPin ConnectionMediaType"); + return E_OUTOFMEMORY; + } + HRESULT STDMETHODCALLTYPE Disconnect() override + { + debug("IPin Disconnect"); + return S_OK; + } + HRESULT STDMETHODCALLTYPE EndFlush() override + { + if (is_output) + return E_UNEXPECTED; + debug("IPin EndFlush"); + return E_OUTOFMEMORY; + } + HRESULT STDMETHODCALLTYPE EndOfStream() override + { + debug("IPin EndOfStream"); + if constexpr (!is_output) + { + parent()->end_of_stream(); + return S_OK; + } + return E_UNEXPECTED; + } + HRESULT STDMETHODCALLTYPE EnumMediaTypes(IEnumMediaTypes** ppEnum) override + { + debug("IPin EnumMediaTypes"); + *ppEnum = nullptr; + return E_OUTOFMEMORY; + } + HRESULT STDMETHODCALLTYPE NewSegment(REFERENCE_TIME tStart, REFERENCE_TIME tStop, double dRate) override + { + debug("IPin NewSegment"); + return S_OK; + } + HRESULT STDMETHODCALLTYPE QueryAccept(const AM_MEDIA_TYPE * pmt) override + { + debug("IPin QueryAccept"); + //if constexpr (!is_output) + //{ + //if (parent()->acceptable_input(nullptr, pmt)) + //return S_OK; + //return VFW_E_TYPE_NOT_ACCEPTED; + //} + return E_UNEXPECTED; + } + HRESULT STDMETHODCALLTYPE QueryDirection(PIN_DIRECTION* pPinDir) override + { + debug("IPin QueryDirection"); + *pPinDir = is_output ? PINDIR_OUTPUT : PINDIR_INPUT; + return S_OK; + } + HRESULT STDMETHODCALLTYPE QueryId(LPWSTR* Id) override + { + debug("IPin QueryId"); + return E_OUTOFMEMORY; + } + HRESULT STDMETHODCALLTYPE QueryInternalConnections(IPin** apPin, ULONG* nPin) override + { + debug("IPin QueryInternalConnections"); + return E_NOTIMPL; + } + HRESULT STDMETHODCALLTYPE QueryPinInfo(PIN_INFO* pInfo) override + { + debug("IPin QueryPinInfo"); + pInfo->pFilter = parent()->parent(); + pInfo->pFilter->AddRef(); + pInfo->dir = is_output ? PINDIR_OUTPUT : PINDIR_INPUT; + wcscpy(pInfo->achName, is_output ? L"source" : L"sink"); + return S_OK; + } + HRESULT STDMETHODCALLTYPE ReceiveConnection(IPin* pConnector, const AM_MEDIA_TYPE * pmt) override + { + debug("IPin ReceiveConnection %s %s %s", guid_to_str(pmt->majortype), guid_to_str(pmt->subtype), guid_to_str(pmt->formattype)); + if constexpr (!is_output) + { + if (parent()->acceptable_input(pConnector, pmt)) + { + puts("INPUT PIN CONNECTED"); + peer = pConnector; + return S_OK; + } + return VFW_E_TYPE_NOT_ACCEPTED; + } + else return E_UNEXPECTED; + } + + // IMemInputPin + HRESULT STDMETHODCALLTYPE GetAllocator(IMemAllocator** ppAllocator) + { + debug("IMemInputPin GetAllocator"); + return VFW_E_NO_ALLOCATOR; + } + HRESULT STDMETHODCALLTYPE GetAllocatorRequirements(ALLOCATOR_PROPERTIES* pProps) + { + debug("IMemInputPin GetAllocatorRequirements"); + return E_NOTIMPL; + } + HRESULT STDMETHODCALLTYPE NotifyAllocator(IMemAllocator* pAllocator, BOOL bReadOnly) + { + debug("IMemInputPin NotifyAllocator, readonly=%u", bReadOnly); + return S_OK; + } + HRESULT STDMETHODCALLTYPE Receive(IMediaSample* pSample) + { + BYTE* ptr; + pSample->GetPointer(&ptr); + size_t size = pSample->GetActualDataLength(); +REFERENCE_TIME time1; +REFERENCE_TIME time2; +pSample->GetTime(&time1, &time2); + debug("IMemInputPin Receive %u at %u %u", (unsigned)size, (unsigned)time1, (unsigned)time2); + parent()->receive_input(ptr, size); + return S_OK; + } + HRESULT STDMETHODCALLTYPE ReceiveCanBlock() + { + debug("IMemInputPin ReceiveCanBlock"); + return E_OUTOFMEMORY; + } + HRESULT STDMETHODCALLTYPE ReceiveMultiple(IMediaSample** pSamples, long nSamples, long* nSamplesProcessed) + { + debug("IMemInputPin ReceiveMultiple"); + return E_OUTOFMEMORY; + } + + // IAsyncReader + // these two exist on IPin already + //HRESULT WINAPI BeginFlush(); + //HRESULT WINAPI EndFlush(); + HRESULT WINAPI Length(int64_t* pTotal, int64_t* pAvailable) + { + debug("IAsyncReader Length"); + return E_OUTOFMEMORY; + } + HRESULT WINAPI Request(IMediaSample* pSample, uintptr_t dwUser) + { + debug("IAsyncReader Request"); + return E_OUTOFMEMORY; + } + HRESULT WINAPI RequestAllocator(IMemAllocator* pPreferred, ALLOCATOR_PROPERTIES* pProps, IMemAllocator** ppActual) + { + debug("IAsyncReader RequestAllocator"); + return E_OUTOFMEMORY; + } + HRESULT WINAPI SyncRead(int64_t llPosition, LONG lLength, uint8_t* pBuffer) + { + debug("IAsyncReader SyncRead %u %u", (unsigned)llPosition, (unsigned)lLength); + return E_OUTOFMEMORY; + } + HRESULT WINAPI SyncReadAligned(IMediaSample* pSample) + { + debug("IAsyncReader SyncReadAligned"); + return E_OUTOFMEMORY; + } + HRESULT WINAPI WaitForNext(DWORD dwTimeout, IMediaSample** ppSample, uintptr_t* pdwUser) + { + debug("IAsyncReader WaitForNext"); + return E_OUTOFMEMORY; + } +}; + +class customFilterSink : public base_filter { +public: + CComPtr dd; + CComPtr dds; + + customFilterSink() + { + DirectDrawCreate(nullptr, &dd, nullptr); + dd->SetCooperativeLevel(GetDesktopWindow(), DDSCL_NORMAL); + DDSURFACEDESC ddsd; + ddsd.dwSize = sizeof(ddsd); + ddsd.dwFlags = DDSD_CAPS;// | DDSD_WIDTH | DDSD_HEIGHT | DDSD_PIXELFORMAT; + ddsd.dwWidth = 640; + ddsd.dwHeight = 540; + ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE; + ddsd.ddpfPixelFormat.dwSize = sizeof(ddsd.ddpfPixelFormat); + ddsd.ddpfPixelFormat.dwFlags = DDPF_LUMINANCE; + ddsd.ddpfPixelFormat.dwLuminanceBitCount = 8; + ddsd.ddpfPixelFormat.dwLuminanceBitMask = 0xFF; + dd->CreateSurface(&ddsd, &dds, NULL); + } + + class in_pin : public base_pin, in_pin> { + public: + customFilterSink* parent() { return container_of<&customFilterSink::pin>(this); } + + bool acceptable_input(IPin* pConnector, const AM_MEDIA_TYPE * pmt) + { + if (pmt->majortype == MEDIATYPE_Video && pmt->formattype == FORMAT_VideoInfo) + return (pmt->subtype == MEDIASUBTYPE_RGB24 || pmt->subtype == MEDIASUBTYPE_YV12); + return false; + } + + void receive_input(uint8_t* ptr, size_t size) + { + IDirectDrawSurface* dds = parent()->dds; + RECT rect = { 0, 0, 640, 540 }; + DDSURFACEDESC surf = { sizeof(DDSURFACEDESC) }; + dds->Lock(&rect, &surf, 0, nullptr); + for (int y=0;y<540;y++) + { + for (int x=0;x<640;x++) + { + uint8_t* px = (uint8_t*)surf.lpSurface + surf.lPitch*y + 4*x; + px[0] = ptr[640*y+x]; + px[1] = ptr[640*y+x]; + px[2] = ptr[640*y+x]; + px[3] = 0; + } + } + dds->Unlock(&rect); + dds->Flip(nullptr, DDFLIP_WAIT); + //static bool first = true; + //if (first) + //{ + //FILE* f = fopen("a.bin", "wb"); + //fwrite(ptr, size,1, f); + //fclose(f); + //first = false; + //} + } + void end_of_stream() + { + //exit(0); + } + }; + in_pin pin; + + IPin* pins[1] = { &pin }; +}; + +class customFilterSource : public base_filter { +public: + class out_pin : public base_pin, out_pin> { + public: + customFilterSource* parent() { return container_of<&customFilterSource::pin>(this); } + + bool connect_output(IPin* pReceivePin) + { + /* + { + CComPtr p; + debug("X=%.8lx", pReceivePin->EnumMediaTypes(&p)); + if (p) + { + AM_MEDIA_TYPE* pmt; + while (p->Next(1, &pmt, nullptr) == S_OK) + { + debug("CANCONNECT %p %p %p\n", guid_to_str(pmt->majortype), guid_to_str(pmt->subtype), guid_to_str(pmt->formattype)); + } + } + } + */ + AM_MEDIA_TYPE mt = { + .majortype = MEDIATYPE_Stream, + .subtype = MEDIASUBTYPE_MPEG1System, + .bFixedSizeSamples = false, + .bTemporalCompression = true, + .lSampleSize = 0, + .formattype = GUID_NULL, + .pUnk = nullptr, + .cbFormat = 0, + .pbFormat = nullptr, + }; + return SUCCEEDED(pReceivePin->ReceiveConnection(this, &mt)); + } + + HRESULT WINAPI Length(int64_t* pTotal, int64_t* pAvailable) + { + debug("IAsyncReader Length"); + *pTotal = parent()->len; + *pAvailable = parent()->len; + return S_OK; + } + HRESULT WINAPI SyncRead(int64_t llPosition, LONG lLength, uint8_t* pBuffer) + { + debug("IAsyncReader SyncRead %u %u", (unsigned)llPosition, (unsigned)lLength); + memcpy(pBuffer, parent()->ptr+llPosition, lLength); + return S_OK; + } + }; + out_pin pin; + + IPin* pins[1] = { &pin }; + + uint8_t* ptr; + size_t len; +}; + +static HRESULT connect_filters(IGraphBuilder* graph, IBaseFilter* src, IBaseFilter* dst) +{ +puts("TRYCONNECTFILT"); + CComPtr src_enum; + if (FAILED(src->EnumPins(&src_enum))) + return E_FAIL; + + CComPtr src_pin; + while (src_enum->Next(1, &src_pin, nullptr) == S_OK) + { + PIN_INFO src_info; + if (FAILED(src_pin->QueryPinInfo(&src_info))) + return E_FAIL; + if (src_info.pFilter) + src_info.pFilter->Release(); +printf("TRYCONNECTSRC "); +for (int i=0;src_info.achName[i];i++)putchar(src_info.achName[i]); +puts(""); + if (src_info.dir != PINDIR_OUTPUT) + continue; + + CComPtr check_pin; + src_pin->ConnectedTo(&check_pin); + if (check_pin != nullptr) + continue; + + CComPtr dst_enum; + dst->EnumPins(&dst_enum); + CComPtr dst_pin; + while (dst_enum->Next(1, &dst_pin, nullptr) == S_OK) + { + PIN_INFO dst_info; + if (FAILED(dst_pin->QueryPinInfo(&dst_info))) + return E_FAIL; +printf("TRYCONNECTDST "); +for (int i=0;src_info.achName[i];i++)putchar(src_info.achName[i]); +puts(""); + if (dst_info.pFilter) + dst_info.pFilter->Release(); + if (dst_info.dir != PINDIR_INPUT) + continue; + + dst_pin->ConnectedTo(&check_pin); + if (check_pin != nullptr) + continue; + +puts("TRYCONNECTPAIR"); + if (SUCCEEDED(graph->ConnectDirect(src_pin, dst_pin, nullptr))) + return S_OK; + } + } + return E_FAIL; +} + +static void require(int seq, HRESULT hr, const char * text = "") +{ + if (SUCCEEDED(hr)) + return; + + printf("fail: %d %.8X%s%s\n", seq, hr, text?" ":"", text); + exit(seq); +} + +int main() +{ + setvbuf(stdout, nullptr, _IONBF, 0); + + if (FAILED(CoInitializeEx(NULL, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE))) + exit(1); + + CComPtr filterGraph; + //CComPtr asyncReader; + //CComPtr asyncReaderFsf; + CComPtr mpegSplitter; + CComPtr mpegVideoCodec; + CComPtr mpegAudioCodec; + CComPtr decodebin; + CComPtr dsound; + CComPtr vmr9; + customFilterSource* source = new customFilterSource(); + customFilterSink* sink = new customFilterSink(); + + require(1, filterGraph.CoCreateInstance(CLSID_FilterGraph, nullptr, CLSCTX_INPROC_SERVER)); + //require(3, asyncReader.CoCreateInstance(CLSID_AsyncReader, nullptr, CLSCTX_INPROC_SERVER)); + require(4, mpegSplitter.CoCreateInstance(CLSID_MPEG1Splitter, nullptr, CLSCTX_INPROC_SERVER)); + require(5, mpegVideoCodec.CoCreateInstance(CLSID_CMpegVideoCodec, nullptr, CLSCTX_INPROC_SERVER)); + require(6, mpegAudioCodec.CoCreateInstance(CLSID_CMpegAudioCodec, nullptr, CLSCTX_INPROC_SERVER)); + require(7, decodebin.CoCreateInstance(CLSID_decodebin_parser, nullptr, CLSCTX_INPROC_SERVER)); + require(8, dsound.CoCreateInstance(CLSID_DSoundRender, nullptr, CLSCTX_INPROC_SERVER)); + require(9, vmr9.CoCreateInstance(CLSID_VideoMixingRenderer9, nullptr, CLSCTX_INPROC_SERVER)); + + require(11, filterGraph->AddFilter(source, L"source")); + require(12, filterGraph->AddFilter(sink, L"sink")); + //require(13, filterGraph->AddFilter(asyncReader, L"asyncReader")); + require(14, filterGraph->AddFilter(mpegSplitter, L"mpegSplitter")); + require(15, filterGraph->AddFilter(mpegVideoCodec, L"mpegVideoCodec")); + require(16, filterGraph->AddFilter(mpegAudioCodec, L"mpegAudioCodec")); + require(17, filterGraph->AddFilter(decodebin, L"decodebin")); + require(18, filterGraph->AddFilter(dsound, L"dsound")); + require(19, filterGraph->AddFilter(vmr9, L"vmr9")); + + //require(20, asyncReader.QueryInterface(&asyncReaderFsf)); + //require(21, asyncReaderFsf->Load(L"video.mpg", nullptr), "failed to open data_video_sample_640x360.mpeg, does the file exist?"); + + FILE* f = fopen("video.mpg", "rb"); + fseek(f, 0, SEEK_END); + size_t f_len = ftell(f); + fseek(f, 0, SEEK_SET); + uint8_t* f_ptr = (uint8_t*)malloc(f_len); + fread(f_ptr, 1,f_len, f); + fclose(f); + source->ptr = f_ptr; + source->len = f_len; + + //require(31, connect_filters(filterGraph, asyncReader, mpegSplitter)); + + //require(31, connect_filters(filterGraph, source, mpegSplitter)); + //require(32, connect_filters(filterGraph, mpegSplitter, mpegVideoCodec)); + //require(33, connect_filters(filterGraph, mpegSplitter, mpegAudioCodec)); + //require(34, connect_filters(filterGraph, mpegVideoCodec, sink)); + //require(35, connect_filters(filterGraph, mpegAudioCodec, dsound)); + + //require(31, connect_filters(filterGraph, source, decodebin)); + //require(32, connect_filters(filterGraph, decodebin, sink)); + //require(33, connect_filters(filterGraph, decodebin, dsound)); + + require(31, connect_filters(filterGraph, source, mpegSplitter)); + require(32, connect_filters(filterGraph, mpegSplitter, mpegVideoCodec)); + require(34, connect_filters(filterGraph, mpegVideoCodec, vmr9)); + require(33, connect_filters(filterGraph, mpegSplitter, mpegAudioCodec)); + require(35, connect_filters(filterGraph, mpegAudioCodec, dsound)); + + //require(31, connect_filters(filterGraph, source, decodebin)); + //require(32, connect_filters(filterGraph, decodebin, vmr9)); + //require(33, connect_filters(filterGraph, decodebin, dsound)); + + puts("connected"); + + //puts("CON1"); + //require(31, connect_filters(filterGraph, source, decodebin)); + //puts("CON2"); + //require(33, connect_filters(filterGraph, decodebin, sink)); + //puts("CON3"); + //require(34, connect_filters(filterGraph, decodebin, dsound)); + //puts("CON4"); + + CComPtr filterGraph_mc; + require(40, filterGraph.QueryInterface(&filterGraph_mc)); + filterGraph_mc->Run(); + puts("running"); + FILTER_STATE st; + printf("WALRUS=%x\n", vmr9->GetState(0, &st)); + printf("WALRUS2=%x\n", st); + SleepEx(8000, true); + + puts("exit"); +} diff --git a/x/messy-runner-custom-vmr9.cpp b/x/messy-runner-custom-vmr9.cpp new file mode 100644 index 0000000..1af62e7 --- /dev/null +++ b/x/messy-runner-custom-vmr9.cpp @@ -0,0 +1,660 @@ +// SPDX-License-Identifier: GPL-2.0-only +// This file contains some code copypasted from Kirikiri . +// Kirikiri is dual licensed under GPLv2 and some homemade license. Since the homemade license is Japanese, +// and I can't read that, I can't accept that license; therefore, this file is GPLv2 only. +// (I'm not sure if headers and other required interopability data is covered by copyright, but better safe than sorry.) +// (I also don't know if Kirikiri is 2.0 only or 2.0 or later, so I'll pick the safe choice.) + +#define INITGUID +#define STRSAFE_NO_DEPRECATE +#include +#include +#include +#include +#include +#include +#include + +static const GUID GUID_NULL = {}; // not defined in my headers, how lovely +DEFINE_GUID(CLSID_decodebin_parser, 0xf9d8d64e, 0xa144, 0x47dc, 0x8e, 0xe0, 0xf5, 0x34, 0x98, 0x37, 0x2c, 0x29); +DEFINE_GUID(CLSID_MPEG1Splitter_Alt, 0x731537a0,0xda85,0x4c5b,0xa4,0xc3,0x45,0x6c,0x49,0x45,0xf8,0xfa); +DEFINE_GUID(CLSID_CMpegVideoCodec_Alt,0xe688d538,0xe607,0x4169,0x86,0xde,0xef,0x08,0x21,0xf5,0xe7,0xa7); + +static char* guid_to_str(const GUID& guid) +{ + static char buf[8][64]; + static int n = 0; + char* ret = buf[n++%8]; + sprintf(ret, "{%08lX-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}", guid.Data1, guid.Data2, guid.Data3, guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3], guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]); + return ret; +} +static wchar_t* guid_to_wstr(const GUID& guid) +{ + static wchar_t buf[8][64] = {}; + char* guids = guid_to_str(guid); + static int n = 0; + wchar_t* ret = buf[n++%8]; + for (int i=0;guids[i];i++) + ret[i] = guids[i]; + return ret; +} + +template class CComPtr { + void assign(T* ptr) + { + p = ptr; + } + void release() + { + if (p) + p->Release(); + p = nullptr; + } +public: + T* p; + + CComPtr() { p = nullptr; } + ~CComPtr() { release(); } + CComPtr(const CComPtr&) = delete; + CComPtr(CComPtr&&) = delete; + void operator=(const CComPtr&) = delete; + void operator=(CComPtr&&) = delete; + + CComPtr& operator=(T* ptr) + { + release(); + assign(ptr); + return *this; + } + T** operator&() + { + release(); + return &p; + } + T* operator->() { return p; } + operator T*() { return p; } + + HRESULT CoCreateInstance(REFCLSID rclsid, LPUNKNOWN pUnkOuter, DWORD dwClsContext) + { + release(); + return ::CoCreateInstance(rclsid, pUnkOuter, dwClsContext, IID_PPV_ARGS(&p)); + } + template + HRESULT QueryInterface(T2** other) + { + return p->QueryInterface(IID_PPV_ARGS(other)); + } + HRESULT CopyTo(T** ppT) + { + p->AddRef(); + *ppT = p; + return S_OK; + } +}; + +static HRESULT connect_filters(IGraphBuilder* graph, IBaseFilter* src, IBaseFilter* dst) +{ +puts("connect."); + CComPtr src_enum; + if (FAILED(src->EnumPins(&src_enum))) + return E_FAIL; + + CComPtr src_pin; + while (src_enum->Next(1, &src_pin, nullptr) == S_OK) + { +puts("src."); + PIN_INFO src_info; + if (FAILED(src_pin->QueryPinInfo(&src_info))) + return E_FAIL; + if (src_info.pFilter) + src_info.pFilter->Release(); + if (src_info.dir != PINDIR_OUTPUT) + continue; + + CComPtr check_pin; + src_pin->ConnectedTo(&check_pin); + if (check_pin != nullptr) + continue; + + CComPtr dst_enum; + dst->EnumPins(&dst_enum); + CComPtr dst_pin; + while (dst_enum->Next(1, &dst_pin, nullptr) == S_OK) + { +puts("dst."); + PIN_INFO dst_info; + if (FAILED(dst_pin->QueryPinInfo(&dst_info))) + return E_FAIL; + if (dst_info.pFilter) + dst_info.pFilter->Release(); + if (dst_info.dir != PINDIR_INPUT) + continue; + + dst_pin->ConnectedTo(&check_pin); + if (check_pin != nullptr) + continue; + +puts("match."); + //if (SUCCEEDED(graph->Connect(src_pin, dst_pin))) + if (SUCCEEDED(graph->ConnectDirect(src_pin, dst_pin, nullptr))) + { +puts("match2."); + return S_OK; + } +puts("match3."); + } + } + return E_FAIL; +} + +static void require(int seq, HRESULT hr, const char * text = "") +{ +printf("%d.\n", seq); + if (SUCCEEDED(hr)) + return; + + printf("fail: %d %.8lX%s%s\n", seq, hr, text?" ":"", text); + exit(seq); +} + + + +static CComPtr filterGraph; + +IBaseFilter* make_filter(IBaseFilter* filt) { return filt; } +IBaseFilter* make_filter(REFIID riid) +{ + IBaseFilter* filt = nullptr; + CoCreateInstance(riid, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&filt)); + if (!filt) + { + printf("failed to create %s\n", guid_to_str(riid)); + exit(1); + } + filterGraph->AddFilter(filt, guid_to_wstr(riid)); + return filt; +} +void connect_chain(IBaseFilter** filts, size_t n) +{ + for (size_t i=0;iQueryFilterInfo(&inf1); + second->QueryFilterInfo(&inf2); + printf("failed to connect %ls to %ls\n", inf1.achName, inf2.achName); + exit(1); + } + } +} +template +IBaseFilter* chain_tail(Ts... args) +{ + IBaseFilter* filts[] = { make_filter(args)... }; + connect_chain(filts, sizeof...(Ts)); + return filts[sizeof...(Ts)-1]; +} +template +void chain(Ts... args) +{ + IBaseFilter* filts[] = { make_filter(args)... }; + connect_chain(filts, sizeof...(Ts)); +} + + + + + + + + +template T first_helper(); +template +class com_base_embedded : public Tis... { +private: + template bool QueryInterfaceSingle(REFIID riid, void** ppvObject) + { + if (riid == __uuidof(Ti)) + { + Ti* ret = this; + ret->AddRef(); + *ppvObject = (void*)ret; + return true; + } + else return false; + } +public: + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override + { +printf("fancy_QI %s -> ", guid_to_str(riid)); + *ppvObject = nullptr; + if (riid == __uuidof(IUnknown)) + { + IUnknown* ret = (decltype(first_helper())*)this; + ret->AddRef(); + *ppvObject = (void*)ret; +puts("ok"); + return S_OK; + } + bool ok = (QueryInterfaceSingle(riid, ppvObject) || ...); +if (ok) puts("ok"); +else puts("fail"); + return ok ? S_OK : E_NOINTERFACE; + } +}; + +template +class com_base : public com_base_embedded { + uint32_t refcount = 1; +public: + ULONG STDMETHODCALLTYPE AddRef() override { return ++refcount; } + ULONG STDMETHODCALLTYPE Release() override + { + uint32_t new_refcount = --refcount; + if (!new_refcount) + delete this; + return new_refcount; + } + virtual ~com_base() {} +}; + +class my_vmr9_allocator : public com_base { +public: + IDirect3D9* d3d; + IDirect3DDevice9* d3ddev; + IVMRSurfaceAllocatorNotify9* allocNotify; + + IDirect3DSurface9* surf[16]; + IDirect3DTexture9* tex; + IDirect3DVertexBuffer9* vertbuf; + CComPtr m_RenderTarget; + HWND wnd; + + struct VideoVertex + { + float x, y, z, w; + float tu, tv; + }; + + void debug(const char * fmt, ...) + { + char buf[1024]; + + va_list args; + va_start(args, fmt); + vsnprintf(buf, 1024, fmt, args); + va_end(args); + + fprintf(stdout, "my_vmr9_allocator %s\n", buf); + fflush(stdout); + } + + HRESULT STDMETHODCALLTYPE InitializeDevice(DWORD_PTR dwUserID, VMR9AllocationInfo* lpAllocInfo, DWORD* lpNumBuffers) + { + debug("IVMRSurfaceAllocator9 InitializeDevice %u %lu, %lux%lu, fmt %lx", (unsigned)dwUserID, *lpNumBuffers, lpAllocInfo->dwWidth, lpAllocInfo->dwHeight, lpAllocInfo->Format); + + D3DCAPS9 d3dcaps; + d3ddev->GetDeviceCaps( &d3dcaps ); + if( d3dcaps.TextureCaps & D3DPTEXTURECAPS_POW2 ) + { puts("walruses??"); + exit(1); + } + + lpAllocInfo->dwFlags |= VMR9AllocFlag_TextureSurface; + + HRESULT hr = allocNotify->AllocateSurfaceHelper(lpAllocInfo, lpNumBuffers, surf); +printf("EXPECTERROR=%lx NBUF=%lu\n", hr, *lpNumBuffers); + + if( FAILED(hr) && !(lpAllocInfo->dwFlags & VMR9AllocFlag_3DRenderTarget) ) + { + if( lpAllocInfo->Format > MAKEFOURCC('0','0','0','0') ) + { + D3DDISPLAYMODE dm; + if( FAILED( hr = (d3ddev->GetDisplayMode( 0, &dm )) ) ) + return hr; + + if( D3D_OK != ( hr = d3ddev->CreateTexture(lpAllocInfo->dwWidth, lpAllocInfo->dwHeight, 1, D3DUSAGE_RENDERTARGET, dm.Format, + D3DPOOL_DEFAULT, &tex, NULL) ) ) + return hr; + puts("krmovie : Use offscreen and YUV surface."); + } else { + puts("krmovie : Use offscreen surface."); + } + lpAllocInfo->dwFlags &= ~VMR9AllocFlag_TextureSurface; + lpAllocInfo->dwFlags |= VMR9AllocFlag_OffscreenSurface; + if( FAILED( hr = allocNotify->AllocateSurfaceHelper(lpAllocInfo, lpNumBuffers, surf ) ) ) + return hr; + } else { + puts("krmovie : Use texture surface."); + } + + + + + D3DDISPLAYMODE dm; + if( FAILED(hr = d3d->GetAdapterDisplayMode( D3DADAPTER_DEFAULT, &dm )) ) + return hr; + + float vtx_l = 0.0f; + float vtx_r = 0.0f; + float vtx_t = 0.0f; + float vtx_b = 0.0f; + + vtx_l = static_cast(0-0); + vtx_t = static_cast(0-0); + vtx_r = vtx_l + static_cast(640); + vtx_b = vtx_t + static_cast(480); + + float tex_w = static_cast(lpAllocInfo->dwWidth); + float tex_h = static_cast(lpAllocInfo->dwWidth); + float video_w = static_cast(lpAllocInfo->dwWidth); + float video_h = static_cast(lpAllocInfo->dwWidth); + if( vtx_r == 0.0f || vtx_b == 0.0f ) { + vtx_r = static_cast(dm.Width); + vtx_b = static_cast(dm.Height); + } + + VideoVertex m_Vtx[4]; + + m_Vtx[0].x = vtx_l - 0.5f; // TL + m_Vtx[0].y = vtx_t - 0.5f; + m_Vtx[0].tu = 0.0f; + m_Vtx[0].tv = 0.0f; + + m_Vtx[1].x = vtx_r - 0.5f; // TR + m_Vtx[1].y = vtx_t - 0.5f; + m_Vtx[1].tu = video_w / tex_w; + m_Vtx[1].tv = 0.0f; + + m_Vtx[2].x = vtx_r - 0.5f; // BR + m_Vtx[2].y = vtx_b - 0.5f; + m_Vtx[2].tu = video_w / tex_w; + m_Vtx[2].tv = video_h / tex_h; + + m_Vtx[3].x = vtx_l - 0.5f; // BL + m_Vtx[3].y = vtx_b - 0.5f; + m_Vtx[3].tu = 0.0f; + m_Vtx[3].tv = video_h / tex_h; + + if( FAILED( hr = d3ddev->CreateVertexBuffer( sizeof(m_Vtx) ,D3DUSAGE_WRITEONLY, D3DFVF_XYZRHW|D3DFVF_TEX1, D3DPOOL_MANAGED, &vertbuf, NULL ) ) ) + return hr; + + void* pData; + if( FAILED( hr = vertbuf->Lock( 0, sizeof(pData), &pData, 0 ) ) ) + return hr; + + memcpy( pData, m_Vtx, sizeof(m_Vtx) ); + + if( FAILED( hr = vertbuf->Unlock() ) ) + return hr; + + return S_OK; + } + + HRESULT STDMETHODCALLTYPE TerminateDevice(DWORD_PTR dwID) + { + debug("IVMRSurfaceAllocator9 TerminateDevice"); + return E_OUTOFMEMORY; + } + HRESULT STDMETHODCALLTYPE GetSurface(DWORD_PTR dwUserID, DWORD SurfaceIndex, DWORD SurfaceFlags, IDirect3DSurface9** lplpSurface) + { + debug("IVMRSurfaceAllocator9 GetSurface"); + surf[SurfaceIndex]->AddRef(); + *lplpSurface = surf[SurfaceIndex]; + return S_OK; + } + HRESULT STDMETHODCALLTYPE AdviseNotify(IVMRSurfaceAllocatorNotify9* lpIVMRSurfAllocNotify) + { + debug("IVMRSurfaceAllocator9 AdviseNotify"); + return E_OUTOFMEMORY; + } + + HRESULT STDMETHODCALLTYPE StartPresenting(DWORD_PTR dwUserID) + { + debug("IVMRImagePresenter9 StartPresenting"); + return S_OK; + } + HRESULT STDMETHODCALLTYPE StopPresenting(DWORD_PTR dwUserID) + { + debug("IVMRImagePresenter9 StopPresenting"); + return S_OK; + } + HRESULT STDMETHODCALLTYPE PresentImage(DWORD_PTR dwUserID, VMR9PresentationInfo* lpPresInfo) + { + debug("IVMRImagePresenter9 PresentImage"); + + HRESULT hr; + CComPtr device; + if( FAILED(hr = lpPresInfo->lpSurf->GetDevice(&device.p )) ) + return hr; + + if( FAILED(hr = device->SetRenderTarget( 0, m_RenderTarget ) ) ) + return hr; + if( tex != NULL ) + { + puts("stretchy"); + CComPtr pSurf; + if( SUCCEEDED(hr = tex->GetSurfaceLevel(0, &pSurf)) ) { +puts("stretchy2"); + if( FAILED(hr = device->StretchRect( lpPresInfo->lpSurf, NULL, pSurf, NULL, D3DTEXF_NONE )) ) { + return hr; + } + } else { + return hr; + } + if( FAILED(hr = DrawVideoPlane( device, tex ) ) ) + return hr; + } else { + CComPtr texture; + if( FAILED(hr = lpPresInfo->lpSurf->GetContainer( IID_IDirect3DTexture9, (LPVOID*)&texture.p ) ) ) + return hr; + if( FAILED(hr = DrawVideoPlane( device, texture.p ) ) ) + return hr; + } + RECT r = { 0, 0, 640, 480 }; + d3ddev->Present( &r, NULL, wnd, NULL ); + return S_OK; + } + + HRESULT DrawVideoPlane( IDirect3DDevice9* device, IDirect3DTexture9* tex ) + { + // device->Clear(0,NULL,D3DCLEAR_TARGET,0,1.0f,0); + if( SUCCEEDED(device->BeginScene()) ) + { + struct CAutoEndSceneCall { + IDirect3DDevice9* m_Device; + CAutoEndSceneCall( IDirect3DDevice9* device ) : m_Device(device) {} + ~CAutoEndSceneCall() { m_Device->EndScene(); } + }; + { + HRESULT hr; + CAutoEndSceneCall autoEnd(device); + if( FAILED( hr = device->SetTexture( 0, tex ) ) ) + return hr; + if( FAILED( hr = device->SetStreamSource(0, vertbuf, 0, sizeof(VideoVertex) ) ) ) + return hr; + if( FAILED( hr = device->SetFVF( D3DFVF_XYZRHW|D3DFVF_TEX1 ) ) ) + return hr; + if( FAILED( hr = device->DrawPrimitive( D3DPT_TRIANGLEFAN, 0, 2 ) ) ) + return hr; + // device->DrawPrimitiveUP( D3DPT_TRIANGLEFAN, 2, reinterpret_cast(m_Vtx), sizeof(m_Vtx[0]) ); + if( FAILED( hr = device->SetTexture( 0, NULL) ) ) + return hr; + } + + //allocNotify->NotifyEvent(EC_UPDATE,0,0); + } + return S_OK; + } +}; + +my_vmr9_allocator vmr9_alloc; + + +int main() +{ + setvbuf(stdout, nullptr, _IONBF, 0); + CoInitializeEx(NULL, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE); + + filterGraph.CoCreateInstance(CLSID_FilterGraph, nullptr, CLSCTX_INPROC_SERVER); + + IBaseFilter* asyncReader = make_filter(CLSID_AsyncReader); + CComPtr asyncReaderFsf; + require(20, asyncReader->QueryInterface(&asyncReaderFsf)); + + require(21, asyncReaderFsf->Load(L"video.mpg", nullptr), "failed to open video.mpg, does the file exist?"); + IBaseFilter* demux = chain_tail(asyncReader, CLSID_MPEG1Splitter); + + + WNDCLASSEXA wcex = { sizeof(WNDCLASSEX), CS_VREDRAW | CS_HREDRAW, DefWindowProcA, 0L, 0L, nullptr, NULL, NULL, NULL, NULL, "windowclass", NULL }; + RegisterClassEx(&wcex); + + HWND wnd = CreateWindowA("windowclass", "VMR9 child", 0, 0, 0, 640, 480, nullptr, NULL, nullptr, NULL ); + ShowWindow(wnd, SW_SHOWDEFAULT); + UpdateWindow(wnd); + +puts("a"); + IBaseFilter* vmr9 = make_filter(CLSID_VideoMixingRenderer9); +puts("c"); + CComPtr pConfig; +puts("d"); + vmr9->QueryInterface(&pConfig); +puts("e"); + pConfig->SetNumberOfStreams(1); +puts("f"); + pConfig->SetRenderingMode(VMR9Mode_Renderless); +puts("g"); + + CComPtr d3d; + d3d.p = Direct3DCreate9(D3D_SDK_VERSION); +printf("d3d=%p\n", d3d.p); + + D3DPRESENT_PARAMETERS d3dpp = {}; + D3DDISPLAYMODE dm; + require(5, d3d->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &dm)); + d3dpp.Windowed = TRUE; + d3dpp.SwapEffect = D3DSWAPEFFECT_COPY; + d3dpp.BackBufferFormat = dm.Format; + d3dpp.BackBufferHeight = 640; + d3dpp.BackBufferWidth = 480; + d3dpp.hDeviceWindow = wnd; + +puts("h"); + CComPtr d3ddev; + DWORD BehaviorFlags = D3DCREATE_SOFTWARE_VERTEXPROCESSING | D3DCREATE_MULTITHREADED; + require(1, d3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, NULL, BehaviorFlags, &d3dpp, &d3ddev)); + + vmr9_alloc.d3d = d3d; + vmr9_alloc.d3ddev = d3ddev; + vmr9_alloc.wnd = wnd; + + require(2, d3ddev->GetRenderTarget( 0, &vmr9_alloc.m_RenderTarget ) ); + + D3DCAPS9 d3dcaps; + require(1, d3ddev->GetDeviceCaps( &d3dcaps ) ); + if( d3dcaps.TextureFilterCaps & D3DPTFILTERCAPS_MAGFLINEAR ) { + require(2, d3ddev->SetSamplerState( 0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR ) ); + } else { + require(3, d3ddev->SetSamplerState( 0, D3DSAMP_MAGFILTER, D3DTEXF_POINT ) ); + } + + if( d3dcaps.TextureFilterCaps & D3DPTFILTERCAPS_MINFLINEAR ) { + require(4, d3ddev->SetSamplerState( 0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR ) ); + } else { + require(5, d3ddev->SetSamplerState( 0, D3DSAMP_MINFILTER, D3DTEXF_POINT ) ); + } + + require(6, d3ddev->SetSamplerState( 0, D3DSAMP_ADDRESSU, D3DTADDRESS_CLAMP ) ); + require(7, d3ddev->SetSamplerState( 0, D3DSAMP_ADDRESSV, D3DTADDRESS_CLAMP ) ); + require(8, d3ddev->SetRenderState( D3DRS_CULLMODE, D3DCULL_NONE ) ); + require(9, d3ddev->SetRenderState( D3DRS_LIGHTING, FALSE ) ); + require(10, d3ddev->SetRenderState( D3DRS_ZENABLE, FALSE ) ); + require(11, d3ddev->SetRenderState( D3DRS_ALPHABLENDENABLE, FALSE ) ); + require(12, d3ddev->SetTextureStageState( 0, D3DTSS_COLOROP, D3DTOP_SELECTARG1 ) ); + require(13, d3ddev->SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_TEXTURE ) ); + require(14, d3ddev->SetTextureStageState( 0, D3DTSS_ALPHAOP, D3DTOP_DISABLE ) ); + + HMONITOR hMonitor = d3d->GetAdapterMonitor(D3DADAPTER_DEFAULT); + + CComPtr allocNotify; +puts("h"); + vmr9->QueryInterface(&allocNotify); + vmr9_alloc.allocNotify = allocNotify; +puts("i"); + allocNotify->AdviseSurfaceAllocator(12345678, &vmr9_alloc); +puts("j"); + allocNotify->SetD3DDevice(d3ddev, hMonitor); +puts("k"); + + chain(demux, CLSID_CMpegVideoCodec, (IBaseFilter*)vmr9); +puts("k"); + chain(demux, CLSID_CMpegAudioCodec, CLSID_DSoundRender); +puts("l"); + + //require(21, asyncReaderFsf->Load(L"waga.wmv", nullptr), "failed to open waga.wmv, does the file exist?"); + //IBaseFilter* demux = make_filter(CLSID_decodebin_parser); + //chain(asyncReader, demux); + //chain(demux, CLSID_VideoMixingRenderer9); + //chain(demux, CLSID_DSoundRender); + + /* + require(3, asyncReader.CoCreateInstance(CLSID_AsyncReader, nullptr, CLSCTX_INPROC_SERVER)); + require(4, mpegSplitter.CoCreateInstance(CLSID_MPEG1Splitter, nullptr, CLSCTX_INPROC_SERVER)); + require(5, mpegVideoCodec.CoCreateInstance(CLSID_CMpegVideoCodec, nullptr, CLSCTX_INPROC_SERVER)); + //require(4, mpegSplitter.CoCreateInstance(CLSID_MPEG1Splitter_Alt, nullptr, CLSCTX_INPROC_SERVER)); + //require(5, mpegVideoCodec.CoCreateInstance(CLSID_CMpegVideoCodec_Alt, nullptr, CLSCTX_INPROC_SERVER)); + require(6, mpegAudioCodec.CoCreateInstance(CLSID_CMpegAudioCodec, nullptr, CLSCTX_INPROC_SERVER)); + require(7, decodebin.CoCreateInstance(CLSID_decodebin_parser, nullptr, CLSCTX_INPROC_SERVER)); + require(8, dsound.CoCreateInstance(CLSID_DSoundRender, nullptr, CLSCTX_INPROC_SERVER)); + require(9, vmr.CoCreateInstance(CLSID_VideoMixingRenderer9, nullptr, CLSCTX_INPROC_SERVER)); + //require(9, vmr.CoCreateInstance(CLSID_VideoMixingRenderer, nullptr, CLSCTX_INPROC_SERVER)); + + require(13, filterGraph->AddFilter(asyncReader, L"asyncReader")); + //require(14, filterGraph->AddFilter(mpegSplitter, L"mpegSplitter")); + //require(15, filterGraph->AddFilter(mpegVideoCodec, L"mpegVideoCodec")); + //require(16, filterGraph->AddFilter(mpegAudioCodec, L"mpegAudioCodec")); + require(17, filterGraph->AddFilter(decodebin, L"decodebin")); + require(18, filterGraph->AddFilter(dsound, L"dsound")); + require(19, filterGraph->AddFilter(vmr, L"vmr")); + + require(20, asyncReader.QueryInterface(&asyncReaderFsf)); + require(21, asyncReaderFsf->Load(L"waga.wmv", nullptr), "failed to open waga.wmv, does the file exist?"); + //require(21, asyncReaderFsf->Load(L"video.mpg", nullptr), "failed to open video.mpg, does the file exist?"); + //require(21, asyncReaderFsf->Load(L"data_video_sample_640x360.mpeg", nullptr), "failed to open data_video_sample_640x360.mpeg, does the file exist?"); + + //require(31, connect_filters(filterGraph, asyncReader, mpegSplitter)); + //require(32, connect_filters(filterGraph, mpegSplitter, mpegVideoCodec)); + //require(33, connect_filters(filterGraph, mpegSplitter, mpegAudioCodec)); + //require(34, connect_filters(filterGraph, mpegVideoCodec, sink)); + //require(35, connect_filters(filterGraph, mpegAudioCodec, dsound)); + + //require(31, connect_filters(filterGraph, asyncReader, decodebin)); + //require(32, connect_filters(filterGraph, decodebin, sink)); + //require(33, connect_filters(filterGraph, decodebin, dsound)); + + //require(31, connect_filters(filterGraph, asyncReader, vmr)); + //require(31, connect_filters(filterGraph, asyncReader, mpegSplitter)); + //require(32, connect_filters(filterGraph, mpegSplitter, mpegVideoCodec)); + //require(34, connect_filters(filterGraph, mpegVideoCodec, vmr)); + //require(33, connect_filters(filterGraph, mpegSplitter, mpegAudioCodec)); + //require(35, connect_filters(filterGraph, mpegAudioCodec, dsound)); + + require(31, connect_filters(filterGraph, asyncReader, decodebin)); + require(32, connect_filters(filterGraph, decodebin, vmr)); + require(33, connect_filters(filterGraph, decodebin, dsound)); + */ + + puts("connected"); + + CComPtr filterGraph_mc; + require(40, filterGraph.QueryInterface(&filterGraph_mc)); + filterGraph_mc->Run(); + puts("running"); + SleepEx(4000, true); + + puts("exit"); +} diff --git a/x/nanodecode-with-krkr-weirdness.cpp b/x/nanodecode-with-krkr-weirdness.cpp new file mode 100644 index 0000000..cc4052e --- /dev/null +++ b/x/nanodecode-with-krkr-weirdness.cpp @@ -0,0 +1,923 @@ +// SPDX-License-Identifier: GPL-2.0-only +// This file contains plenty of headers and function prototypes copypasted from Kirikiri . +// Kirikiri is dual licensed under GPLv2 and some homemade license. Since the homemade license is Japanese, +// and I can't read that, I can't accept that license; therefore, this file is GPLv2 only. +// (I'm not sure if headers and other required interopability data is covered by copyright, but better safe than sorry.) + +#define INITGUID +#define STRSAFE_NO_DEPRECATE +#include +#include +#include +#include +#include +#include +#include + +static const GUID GUID_NULL = {}; // not defined in my headers, how lovely +DEFINE_GUID(CLSID_decodebin_parser, 0xf9d8d64e, 0xa144, 0x47dc, 0x8e, 0xe0, 0xf5, 0x34, 0x98, 0x37, 0x2c, 0x29); +DEFINE_GUID(CLSID_MPEG1Splitter_Alt, 0x731537a0,0xda85,0x4c5b,0xa4,0xc3,0x45,0x6c,0x49,0x45,0xf8,0xfa); +DEFINE_GUID(CLSID_CMpegVideoCodec_Alt,0xe688d538,0xe607,0x4169,0x86,0xde,0xef,0x08,0x21,0xf5,0xe7,0xa7); +__CRT_UUID_DECL(IMediaEventEx,0x56a868c0,0x0ad4,0x11ce,0xb0,0x3a,0x00,0x20,0xaf,0x0b,0xa7,0x70); + +static char* guid_to_str(const GUID& guid) +{ + static char buf[8][64]; + static int n = 0; + char* ret = buf[n++%8]; + sprintf(ret, "{%08lX-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}", guid.Data1, guid.Data2, guid.Data3, guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3], guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]); + return ret; +} +static wchar_t* guid_to_wstr(const GUID& guid) +{ + static wchar_t buf[8][64] = {}; + char* guids = guid_to_str(guid); + static int n = 0; + wchar_t* ret = buf[n++%8]; + for (int i=0;guids[i];i++) + ret[i] = guids[i]; + return ret; +} + +template class CComPtr { + void assign(T* ptr) + { + p = ptr; + } + void release() + { + if (p) + p->Release(); + p = nullptr; + } +public: + T* p; + + CComPtr() { p = nullptr; } + ~CComPtr() { release(); } + CComPtr(const CComPtr&) = delete; + CComPtr(CComPtr&&) = delete; + void operator=(const CComPtr&) = delete; + void operator=(CComPtr&&) = delete; + + CComPtr& operator=(T* ptr) + { + release(); + assign(ptr); + return *this; + } + T** operator&() + { + release(); + return &p; + } + T* operator->() { return p; } + operator T*() { return p; } + + HRESULT CoCreateInstance(REFCLSID rclsid, LPUNKNOWN pUnkOuter, DWORD dwClsContext) + { + release(); + return ::CoCreateInstance(rclsid, pUnkOuter, dwClsContext, IID_PPV_ARGS(&p)); + } + template + HRESULT QueryInterface(T2** other) + { + return p->QueryInterface(IID_PPV_ARGS(other)); + } + HRESULT CopyTo(T** ppT) + { + p->AddRef(); + *ppT = p; + return S_OK; + } +}; + +static HRESULT connect_filters(IGraphBuilder* graph, IBaseFilter* src, IBaseFilter* dst) +{ +puts("connect."); + CComPtr src_enum; + if (FAILED(src->EnumPins(&src_enum))) + return E_FAIL; + + CComPtr src_pin; + while (src_enum->Next(1, &src_pin, nullptr) == S_OK) + { +puts("src."); + PIN_INFO src_info; + if (FAILED(src_pin->QueryPinInfo(&src_info))) + return E_FAIL; + if (src_info.pFilter) + src_info.pFilter->Release(); + if (src_info.dir != PINDIR_OUTPUT) + continue; + + CComPtr check_pin; + src_pin->ConnectedTo(&check_pin); + if (check_pin != nullptr) + continue; + + CComPtr dst_enum; + dst->EnumPins(&dst_enum); + CComPtr dst_pin; + while (dst_enum->Next(1, &dst_pin, nullptr) == S_OK) + { +puts("dst."); + PIN_INFO dst_info; + if (FAILED(dst_pin->QueryPinInfo(&dst_info))) + return E_FAIL; + if (dst_info.pFilter) + dst_info.pFilter->Release(); + if (dst_info.dir != PINDIR_INPUT) + continue; + + dst_pin->ConnectedTo(&check_pin); + if (check_pin != nullptr) + continue; + +puts("match."); + //if (SUCCEEDED(graph->Connect(src_pin, dst_pin))) + if (SUCCEEDED(graph->ConnectDirect(src_pin, dst_pin, nullptr))) + { +puts("match2."); + return S_OK; + } +puts("match3."); + } + } + return E_FAIL; +} + +static void require(int seq, HRESULT hr, const char * text = "") +{ +printf("%d.\n", seq); + if (SUCCEEDED(hr)) + return; + + printf("fail: %d %.8lX%s%s\n", seq, hr, text?" ":"", text); + exit(seq); +} + + + +static CComPtr filterGraph; + +IBaseFilter* try_make_filter(REFIID riid) +{ + IBaseFilter* filt = nullptr; + CoCreateInstance(riid, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&filt)); + if (!filt) + return nullptr; + filterGraph->AddFilter(filt, guid_to_wstr(riid)); + return filt; +} +IBaseFilter* make_filter(IBaseFilter* filt) { return filt; } +IBaseFilter* make_filter(REFIID riid) +{ + IBaseFilter* filt = try_make_filter(riid); + if (!filt) + { + printf("failed to create %s\n", guid_to_str(riid)); + exit(1); + } + return filt; +} +bool connect_chain(IBaseFilter** filts, size_t n, bool required) +{ + for (size_t i=0;iQueryFilterInfo(&inf1); + second->QueryFilterInfo(&inf2); + printf("failed to connect %ls to %ls\n", inf1.achName, inf2.achName); + exit(1); + } + } + return true; +} +template +IBaseFilter* chain_tail(Ts... args) +{ + IBaseFilter* filts[] = { make_filter(args)... }; + connect_chain(filts, sizeof...(Ts), true); + return filts[sizeof...(Ts)-1]; +} +template +void chain(Ts... args) +{ + IBaseFilter* filts[] = { make_filter(args)... }; + connect_chain(filts, sizeof...(Ts), true); +} +template +bool try_chain(Ts... args) +{ + IBaseFilter* filts[] = { make_filter(args)... }; + return connect_chain(filts, sizeof...(Ts), false); +} + + + + + + + + +template T first_helper(); +template +class com_base_embedded : public Tis... { +private: + template bool QueryInterfaceSingle(REFIID riid, void** ppvObject) + { + if (riid == __uuidof(Ti)) + { + Ti* ret = this; + ret->AddRef(); + *ppvObject = (void*)ret; + return true; + } + else return false; + } +public: + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override + { +printf("fancy_QI %s -> ", guid_to_str(riid)); + *ppvObject = nullptr; + if (riid == __uuidof(IUnknown)) + { + IUnknown* ret = (decltype(first_helper())*)this; + ret->AddRef(); + *ppvObject = (void*)ret; +puts("ok"); + return S_OK; + } + bool ok = (QueryInterfaceSingle(riid, ppvObject) || ...); +if (ok) puts("ok"); +else puts("fail"); + return ok ? S_OK : E_NOINTERFACE; + } +}; + +template +class com_base : public com_base_embedded { + uint32_t refcount = 1; +public: + ULONG STDMETHODCALLTYPE AddRef() override { return ++refcount; } + ULONG STDMETHODCALLTYPE Release() override + { + uint32_t new_refcount = --refcount; + if (!new_refcount) + delete this; + return new_refcount; + } + virtual ~com_base() {} +}; + +#define WM_GRAPHNOTIFY (WM_USER+15) +#define WM_CALLBACKCMD (WM_USER+16) +#define EC_UPDATE (EC_USER+1) + +bool dump_texture(IDirect3DSurface9* renderTarget, const char * filename) +{ + HRESULT hr; + IDirect3DDevice9* dev; + //hr = dev->GetRenderTarget( 0, &renderTarget ); + hr = renderTarget->GetDevice(&dev); + //hr = lpPresInfo->lpSurf->GetSurfaceLevel( 0, &renderTarget ); + if( !renderTarget || FAILED(hr) ) + return false; + + D3DSURFACE_DESC rtDesc; + renderTarget->GetDesc( &rtDesc ); + + IDirect3DSurface9* resolvedSurface; + if( rtDesc.MultiSampleType != D3DMULTISAMPLE_NONE ) + { + hr = dev->CreateRenderTarget( rtDesc.Width, rtDesc.Height, rtDesc.Format, D3DMULTISAMPLE_NONE, 0, FALSE, &resolvedSurface, NULL ); + if( FAILED(hr) ) + return false; + hr = dev->StretchRect( renderTarget, NULL, resolvedSurface, NULL, D3DTEXF_NONE ); + if( FAILED(hr) ) + return false; + renderTarget = resolvedSurface; + } + + IDirect3DSurface9* offscreenSurface; + //hr = dev->CreateOffscreenPlainSurface( rtDesc.Width, rtDesc.Height, rtDesc.Format, D3DPOOL_SYSTEMMEM, &offscreenSurface, NULL ); + hr = dev->CreateOffscreenPlainSurface( rtDesc.Width, rtDesc.Height, D3DFMT_X8R8G8B8, D3DPOOL_SYSTEMMEM, &offscreenSurface, NULL ); + if( FAILED(hr) ) + return false; + + hr = dev->GetRenderTargetData( renderTarget, offscreenSurface ); + bool ok = SUCCEEDED(hr); + if( ok ) + { + // Here we have data in offscreenSurface. + D3DLOCKED_RECT lr; + RECT rect; + rect.left = 0; + rect.right = rtDesc.Width; + rect.top = 0; + rect.bottom = rtDesc.Height; + // Lock the surface to read pixels + hr = offscreenSurface->LockRect( &lr, &rect, D3DLOCK_READONLY ); + if( SUCCEEDED(hr) ) + { +FILE* f=fopen(filename, "wb"); +for (size_t i=0;iUnlockRect(); + } + else + { + ok = false; + } + } + return ok; +} + +class my_vmr9_allocator : public com_base { +public: + IDirect3D9* d3d; + IDirect3DDevice9* d3ddev; + IVMRSurfaceAllocatorNotify9* allocNotify; + + IDirect3DSurface9* surf[16]; + IDirect3DTexture9* tex = nullptr; + //IDirect3DVertexBuffer9* vertbuf; + CComPtr m_VertexBuffer; + IMediaEventEx* event; + CComPtr m_RenderTarget; + HWND wnd; + + struct VideoVertex + { + float x, y, z, w; + float tu, tv; + }; + + void debug(const char * fmt, ...) + { + char buf[1024]; + + va_list args; + va_start(args, fmt); + vsnprintf(buf, 1024, fmt, args); + va_end(args); + + fprintf(stdout, "my_vmr9_allocator %s\n", buf); + fflush(stdout); + } + + HRESULT STDMETHODCALLTYPE InitializeDevice(DWORD_PTR dwUserID, VMR9AllocationInfo* lpAllocInfo, DWORD* lpNumBuffers) + { + debug("IVMRSurfaceAllocator9 InitializeDevice %u %lu, %lux%lu, fmt %lx", (unsigned)dwUserID, *lpNumBuffers, lpAllocInfo->dwWidth, lpAllocInfo->dwHeight, lpAllocInfo->Format); + + D3DCAPS9 d3dcaps; + d3ddev->GetDeviceCaps( &d3dcaps ); + if( d3dcaps.TextureCaps & D3DPTEXTURECAPS_POW2 ) + { + puts("D3DPTEXTURECAPS_POW2 enabled??"); + exit(1); + } + + lpAllocInfo->dwFlags |= VMR9AllocFlag_TextureSurface; + + HRESULT hr = allocNotify->AllocateSurfaceHelper(lpAllocInfo, lpNumBuffers, surf); +printf("EXPECTERROR=%lx NBUF=%lu\n", hr, *lpNumBuffers); + + if( FAILED(hr) && !(lpAllocInfo->dwFlags & VMR9AllocFlag_3DRenderTarget) ) + { + if( lpAllocInfo->Format > MAKEFOURCC('0','0','0','0') ) + { + D3DDISPLAYMODE dm; + if( FAILED( hr = (d3ddev->GetDisplayMode( 0, &dm )) ) ) + return hr; + + if( D3D_OK != ( hr = d3ddev->CreateTexture(lpAllocInfo->dwWidth, lpAllocInfo->dwHeight, 1, D3DUSAGE_RENDERTARGET, dm.Format, + D3DPOOL_DEFAULT, &tex, NULL) ) ) + return hr; + puts("krmovie : Use offscreen and YUV surface."); + } else { + puts("krmovie : Use offscreen surface."); + } + lpAllocInfo->dwFlags &= ~VMR9AllocFlag_TextureSurface; + lpAllocInfo->dwFlags |= VMR9AllocFlag_OffscreenSurface; + if( FAILED( hr = allocNotify->AllocateSurfaceHelper(lpAllocInfo, lpNumBuffers, surf ) ) ) + return hr; + } else { + puts("krmovie : Use texture surface."); + } + + + + + D3DDISPLAYMODE dm; + if( FAILED(hr = d3d->GetAdapterDisplayMode( D3DADAPTER_DEFAULT, &dm )) ) + return hr; + +/* + float srcvertices[]={ + -1,-1,0, 0,1, + -1, 1,0, 0,0, + 1,-1,0, 1,1, + 1, 1,0, 1,0, + }; + if( FAILED( hr = d3ddev->CreateVertexBuffer( sizeof(srcvertices) ,D3DUSAGE_WRITEONLY, D3DFVF_XYZRHW|D3DFVF_TEX1, D3DPOOL_MANAGED, &vertbuf, NULL ) ) ) + return hr; + + void* pData; + if( FAILED( hr = vertbuf->Lock( 0, sizeof(srcvertices), &pData, 0 ) ) ) + return hr; + + //memcpy( pData, m_Vtx, sizeof(m_Vtx) ); + memcpy( pData, srcvertices, sizeof(srcvertices) ); + + if( FAILED( hr = vertbuf->Unlock() ) ) + return hr; +*/ + + float vtx_l = 0.0f; + float vtx_r = 0.0f; + float vtx_t = 0.0f; + float vtx_b = 0.0f; + + vtx_l = static_cast(0 - 0); + vtx_t = static_cast(0 - 0); + vtx_r = vtx_l + static_cast(lpAllocInfo->dwWidth-0); + vtx_b = vtx_t + static_cast(lpAllocInfo->dwHeight-0); + + float tex_w = static_cast(lpAllocInfo->dwWidth); + float tex_h = static_cast(lpAllocInfo->dwHeight); + float video_w = static_cast(lpAllocInfo->dwWidth); + float video_h = static_cast(lpAllocInfo->dwHeight); + if( vtx_r == 0.0f || vtx_b == 0.0f ) { + vtx_r = static_cast(lpAllocInfo->dwWidth); + vtx_b = static_cast(lpAllocInfo->dwHeight); + } + + VideoVertex m_Vtx[4]; + m_Vtx[0].z = m_Vtx[1].z = m_Vtx[2].z = m_Vtx[3].z = 1.0f; + m_Vtx[0].w = m_Vtx[1].w = m_Vtx[2].w = m_Vtx[3].w = 1.0f; + + m_Vtx[0].x = vtx_l - 0.5f; // TL + m_Vtx[0].y = vtx_t - 0.5f; + m_Vtx[0].tu = 0.0f; + m_Vtx[0].tv = 0.0f; + + m_Vtx[1].x = vtx_r - 0.5f; // TR + m_Vtx[1].y = vtx_t - 0.5f; + m_Vtx[1].tu = video_w / tex_w; + m_Vtx[1].tv = 0.0f; + + m_Vtx[2].x = vtx_r - 0.5f; // BR + m_Vtx[2].y = vtx_b - 0.5f; + m_Vtx[2].tu = video_w / tex_w; + m_Vtx[2].tv = video_h / tex_h; + + m_Vtx[3].x = vtx_l - 0.5f; // BL + m_Vtx[3].y = vtx_b - 0.5f; + m_Vtx[3].tu = 0.0f; + m_Vtx[3].tv = video_h / tex_h; + + m_VertexBuffer = NULL; + if( FAILED( hr = d3ddev->CreateVertexBuffer( sizeof(m_Vtx) ,D3DUSAGE_WRITEONLY, D3DFVF_XYZRHW|D3DFVF_TEX1, D3DPOOL_MANAGED, &m_VertexBuffer.p, NULL ) ) ) + return hr; + + void* pData; + if( FAILED( hr = m_VertexBuffer->Lock( 0, sizeof(pData), &pData, 0 ) ) ) + return hr; + + memcpy( pData, m_Vtx, sizeof(m_Vtx) ); + + if( FAILED( hr = m_VertexBuffer->Unlock() ) ) + return hr; + + + + return S_OK; + } + + HRESULT STDMETHODCALLTYPE TerminateDevice(DWORD_PTR dwID) + { + debug("IVMRSurfaceAllocator9 TerminateDevice"); + return S_OK; + } + HRESULT STDMETHODCALLTYPE GetSurface(DWORD_PTR dwUserID, DWORD SurfaceIndex, DWORD SurfaceFlags, IDirect3DSurface9** lplpSurface) + { + debug("IVMRSurfaceAllocator9 GetSurface"); + surf[SurfaceIndex]->AddRef(); + *lplpSurface = surf[SurfaceIndex]; + return S_OK; + } + //HRESULT STDMETHODCALLTYPE GetSurfaceEx(DWORD_PTR dwUserID, DWORD SurfaceIndex, DWORD SurfaceFlags, IDirect3DSurface9** lplpSurface, RECT* lprcDst) + //{ + //debug("IVMRSurfaceAllocator9 GetSurface"); + //surf[SurfaceIndex]->AddRef(); + //*lplpSurface = surf[SurfaceIndex]; + //*lprcDst = { 0, 0, 640, 480 }; + //return S_OK; + //} + HRESULT STDMETHODCALLTYPE AdviseNotify(IVMRSurfaceAllocatorNotify9* lpIVMRSurfAllocNotify) + { + debug("IVMRSurfaceAllocator9 AdviseNotify"); + return E_OUTOFMEMORY; + } + + HRESULT STDMETHODCALLTYPE StartPresenting(DWORD_PTR dwUserID) + { + debug("IVMRImagePresenter9 StartPresenting"); + return S_OK; + } + HRESULT STDMETHODCALLTYPE StopPresenting(DWORD_PTR dwUserID) + { + debug("IVMRImagePresenter9 StopPresenting"); + return S_OK; + } + HRESULT STDMETHODCALLTYPE PresentImage(DWORD_PTR dwUserID, VMR9PresentationInfo* lpPresInfo) + { + HRESULT hr = PresentImage_(dwUserID, lpPresInfo); + if (hr != S_OK) + { + puts("EEE?"); + exit(1); + } + return hr; + } + HRESULT STDMETHODCALLTYPE PresentImage_(DWORD_PTR dwUserID, VMR9PresentationInfo* lpPresInfo) + { + debug("IVMRImagePresenter9 PresentImage"); +static int iz=0; +iz++; +if (iz == 100) +{ + //IDirect3DTexture9* pTexture; + //IDirect3DSurface9* renderTarget; + //HRESULT hr = lpPresInfo->lpSurf->GetContainer(IID_IDirect3DTexture9, (void**)&pTexture); + //pTexture->GetSurfaceLevel( 0, &renderTarget ); + dump_texture(lpPresInfo->lpSurf, "a.bin"); +//exit(1); +} + + HRESULT hr; + CComPtr device; + if( FAILED(hr = lpPresInfo->lpSurf->GetDevice(&device.p )) ) + return hr; + + if( FAILED(hr = device->SetRenderTarget( 0, m_RenderTarget ) ) ) + return hr; + if( tex != NULL ) + { + puts("stretchy"); + CComPtr pSurf; + if( SUCCEEDED(hr = tex->GetSurfaceLevel(0, &pSurf)) ) { +puts("stretchy2"); + if( FAILED(hr = device->StretchRect( lpPresInfo->lpSurf, NULL, pSurf, NULL, D3DTEXF_NONE )) ) { + return hr; + } + } else { + return hr; + } + if( FAILED(hr = DrawVideoPlane( device, tex ) ) ) + return hr; + } else { + CComPtr texture; + if( FAILED(hr = lpPresInfo->lpSurf->GetContainer( IID_IDirect3DTexture9, (LPVOID*)&texture.p ) ) ) + return hr; + if( FAILED(hr = DrawVideoPlane( device, texture.p ) ) ) + return hr; + } + return S_OK; + } + + HRESULT DrawVideoPlane( IDirect3DDevice9* device, IDirect3DTexture9* tex ) + { + //d3ddev->Clear(0,NULL,D3DCLEAR_TARGET,0,1.0f,0); + d3ddev->Clear(0,NULL,D3DCLEAR_TARGET,D3DCOLOR_RGBA(0,255,0,0),1.0f,0); + //RECT r = { 0, 0, 640, 480 }; + //require(1001, d3ddev->Present( &r, NULL, wnd, NULL )); + //require(1001, d3ddev->Present( nullptr, NULL, nullptr, NULL )); + //return S_OK; + if( SUCCEEDED(device->BeginScene()) ) + { + struct CAutoEndSceneCall { + IDirect3DDevice9* m_Device; + CAutoEndSceneCall( IDirect3DDevice9* device ) : m_Device(device) {} + ~CAutoEndSceneCall() { m_Device->EndScene(); } + }; + { + CAutoEndSceneCall autoEnd(device); + + HRESULT hr; + if( FAILED( hr = device->SetTexture( 0, tex ) ) ) + return hr; + if( FAILED( hr = device->SetStreamSource(0, m_VertexBuffer.p, 0, sizeof(VideoVertex) ) ) ) + return hr; + if( FAILED( hr = device->SetFVF( D3DFVF_XYZRHW|D3DFVF_TEX1 ) ) ) + return hr; + if( FAILED( hr = device->DrawPrimitive( D3DPT_TRIANGLEFAN, 0, 2 ) ) ) + return hr; +// device->DrawPrimitiveUP( D3DPT_TRIANGLEFAN, 2, reinterpret_cast(m_Vtx), sizeof(m_Vtx[0]) ); + if( FAILED( hr = device->SetTexture( 0, NULL) ) ) + return hr; +static int iz=0; +iz++; +if (iz == 100) +{ + IDirect3DSurface9* renderTarget; + //HRESULT hr = d3ddev->GetRenderTarget( 0, &renderTarget ); + tex->GetSurfaceLevel( 0, &renderTarget ); + dump_texture(renderTarget, "b.bin"); +//while (true) Sleep(100000); +exit(1); +} + +/* + device->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); + device->SetRenderState(D3DRS_LIGHTING, FALSE); + device->SetTexture(0, tex);//apparently this one is subclassed + device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1); + device->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); + device->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_DISABLE); + + device->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_POINT); + device->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_POINT); + device->SetSamplerState(0, D3DSAMP_MIPFILTER, D3DTEXF_POINT); + + device->SetStreamSource(0, vertbuf, 0, sizeof(float)*5); + device->SetFVF(D3DFVF_XYZ|D3DFVF_TEX1); + device->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2); +*/ + } + + RECT r = { 0, 0, 640, 480 }; + require(1001, d3ddev->Present( &r, NULL, wnd, NULL )); + //require(1001, d3ddev->Present( nullptr, NULL, nullptr, NULL )); + //allocNotify->NotifyEvent(EC_UPDATE,0,0); + } + return S_OK; + } +}; + +my_vmr9_allocator vmr9_alloc; + + +LRESULT myWindowProcA(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) +{ + switch (Msg) + { + case WM_GRAPHNOTIFY: + { + puts("NOTIFIED"); + long evcode; + LONG_PTR p1; + LONG_PTR p2; + while (SUCCEEDED(vmr9_alloc.event->GetEvent(&evcode, &p1, &p2, 0))) + { + printf("NOTIFIED2 %ld %ld %ld\n", evcode, (long)p1, (long)p2); + if (evcode == EC_UPDATE) + { + puts("NOTIFIED3"); + require(1001, vmr9_alloc.d3ddev->Present( nullptr, NULL, nullptr, NULL )); + //RECT r = { 0, 0, 640, 480 }; + //require(1001, vmr9_alloc.d3ddev->Present( &r, NULL, vmr9_alloc.wnd, NULL )); + } + vmr9_alloc.event->FreeEventParams( evcode, p1, p2 ); + } + } + break; + case WM_CLOSE: + PostQuitMessage(0); + break; + } + return DefWindowProcA(hWnd, Msg, wParam, lParam); +} + +int main() +{ + setvbuf(stdout, nullptr, _IONBF, 0); + CoInitializeEx(NULL, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE); + + filterGraph.CoCreateInstance(CLSID_FilterGraph, nullptr, CLSCTX_INPROC_SERVER); + + IBaseFilter* asyncReader = make_filter(CLSID_AsyncReader); + CComPtr asyncReaderFsf; + require(20, asyncReader->QueryInterface(&asyncReaderFsf)); + + require(21, asyncReaderFsf->Load(L"video.mpg", nullptr), "failed to open video.mpg, does the file exist?"); + //require(21, asyncReaderFsf->Load(L"waga.wmv", nullptr), "failed to open waga.wmv, does the file exist?"); + IBaseFilter* demux = try_make_filter(CLSID_MPEG1Splitter); + bool is_decodebin = false; + if (!demux) + { + puts("CLSID_MPEG1Splitter not available? Probably running on Wine, trying CLSID_decodebin_parser instead"); + demux = make_filter(CLSID_decodebin_parser); + is_decodebin = true; + } + chain(asyncReader, demux); + + WNDCLASSEXA wcex = { sizeof(WNDCLASSEX), CS_VREDRAW | CS_HREDRAW, myWindowProcA, 0L, 0L, nullptr, NULL, NULL, NULL, NULL, "windowclass", NULL }; + RegisterClassEx(&wcex); + + HWND wnd = CreateWindowA("windowclass", "VMR9 child", 0, 0, 0, 680, 520, nullptr, NULL, nullptr, NULL ); + ShowWindow(wnd, SW_SHOWDEFAULT); + UpdateWindow(wnd); + + /* + WNDCLASSEXA wcex = { sizeof(WNDCLASSEX), CS_VREDRAW | CS_HREDRAW, myWindowProcA, 0L, 0L, nullptr, NULL, NULL, NULL, NULL, "windowclass", NULL }; + RegisterClassEx(&wcex); + + WNDCLASSEXA wcex2 = { sizeof(WNDCLASSEX), CS_PARENTDC | CS_VREDRAW | CS_HREDRAW, myWindowProcA, 0L, 0L, nullptr, NULL, NULL, NULL, NULL, "windowclass_child", NULL }; + RegisterClassEx(&wcex2); + + HWND wnd_parent = CreateWindowA("windowclass", "VMR9 parent", 0, 0, 0, 680, 520, nullptr, NULL, nullptr, NULL ); + ShowWindow(wnd_parent, SW_SHOWDEFAULT); + UpdateWindow(wnd_parent); + + HWND wnd = CreateWindowA("windowclass_child", "VMR9 child", WS_CHILD, 0, 0, 660, 500, wnd_parent, NULL, nullptr, NULL ); + ShowWindow(wnd, SW_SHOWDEFAULT); + UpdateWindow(wnd); + */ + +puts("a"); + IBaseFilter* vmr9 = make_filter(CLSID_VideoMixingRenderer9); +puts("c"); + CComPtr pConfig; +puts("d"); + vmr9->QueryInterface(&pConfig); +puts("e"); + pConfig->SetNumberOfStreams(1); +puts("f"); + pConfig->SetRenderingMode(VMR9Mode_Renderless); +puts("g"); + + CComPtr d3d; + d3d.p = Direct3DCreate9(D3D_SDK_VERSION); +printf("d3d=%p\n", d3d.p); + + D3DPRESENT_PARAMETERS d3dpp = {}; + D3DDISPLAYMODE dm; + require(5, d3d->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &dm)); + d3dpp.Windowed = TRUE; + d3dpp.SwapEffect = D3DSWAPEFFECT_COPY; + d3dpp.BackBufferFormat = dm.Format; + d3dpp.BackBufferHeight = 640; + d3dpp.BackBufferWidth = 480; + d3dpp.hDeviceWindow = wnd; + +puts("h"); + CComPtr d3ddev; + DWORD BehaviorFlags = D3DCREATE_SOFTWARE_VERTEXPROCESSING | D3DCREATE_MULTITHREADED; + require(1, d3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, NULL, BehaviorFlags, &d3dpp, &d3ddev)); + + vmr9_alloc.d3d = d3d; + vmr9_alloc.d3ddev = d3ddev; + vmr9_alloc.wnd = wnd; + + require(2, d3ddev->GetRenderTarget( 0, &vmr9_alloc.m_RenderTarget ) ); + + D3DCAPS9 d3dcaps; + require(1, d3ddev->GetDeviceCaps( &d3dcaps ) ); + if( d3dcaps.TextureFilterCaps & D3DPTFILTERCAPS_MAGFLINEAR ) { + require(2, d3ddev->SetSamplerState( 0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR ) ); + } else { + require(3, d3ddev->SetSamplerState( 0, D3DSAMP_MAGFILTER, D3DTEXF_POINT ) ); + } + + if( d3dcaps.TextureFilterCaps & D3DPTFILTERCAPS_MINFLINEAR ) { + require(4, d3ddev->SetSamplerState( 0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR ) ); + } else { + require(5, d3ddev->SetSamplerState( 0, D3DSAMP_MINFILTER, D3DTEXF_POINT ) ); + } + + require(6, d3ddev->SetSamplerState( 0, D3DSAMP_ADDRESSU, D3DTADDRESS_CLAMP ) ); + require(7, d3ddev->SetSamplerState( 0, D3DSAMP_ADDRESSV, D3DTADDRESS_CLAMP ) ); + require(8, d3ddev->SetRenderState( D3DRS_CULLMODE, D3DCULL_NONE ) ); + require(9, d3ddev->SetRenderState( D3DRS_LIGHTING, FALSE ) ); + require(10, d3ddev->SetRenderState( D3DRS_ZENABLE, FALSE ) ); + require(11, d3ddev->SetRenderState( D3DRS_ALPHABLENDENABLE, FALSE ) ); + require(12, d3ddev->SetTextureStageState( 0, D3DTSS_COLOROP, D3DTOP_SELECTARG1 ) ); + require(13, d3ddev->SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_TEXTURE ) ); + require(14, d3ddev->SetTextureStageState( 0, D3DTSS_ALPHAOP, D3DTOP_DISABLE ) ); + + HMONITOR hMonitor = d3d->GetAdapterMonitor(D3DADAPTER_DEFAULT); + + CComPtr allocNotify; +puts("h"); + vmr9->QueryInterface(&allocNotify); + vmr9_alloc.allocNotify = allocNotify; +puts("i"); + allocNotify->AdviseSurfaceAllocator(0, &vmr9_alloc); +puts("j"); + allocNotify->SetD3DDevice(d3ddev, hMonitor); +puts("k"); + + if (!is_decodebin) + chain(demux, CLSID_CMpegVideoCodec, (IBaseFilter*)vmr9); + else + chain(demux, (IBaseFilter*)vmr9); +puts("k"); + if (!is_decodebin) + try_chain(demux, CLSID_CMpegAudioCodec, CLSID_DSoundRender); // don't worry too much if the file doesn't have sound + else + try_chain(demux, CLSID_DSoundRender); +puts("l"); + + CComPtr event; + require(120, filterGraph.QueryInterface(&event)); + require(123, event->SetNotifyWindow((OAHWND)wnd, WM_GRAPHNOTIFY, 0)); + vmr9_alloc.event = event; + + //require(21, asyncReaderFsf->Load(L"waga.wmv", nullptr), "failed to open waga.wmv, does the file exist?"); + //IBaseFilter* demux = make_filter(CLSID_decodebin_parser); + //chain(asyncReader, demux); + //chain(demux, CLSID_VideoMixingRenderer9); + //chain(demux, CLSID_DSoundRender); + + /* + require(3, asyncReader.CoCreateInstance(CLSID_AsyncReader, nullptr, CLSCTX_INPROC_SERVER)); + require(4, mpegSplitter.CoCreateInstance(CLSID_MPEG1Splitter, nullptr, CLSCTX_INPROC_SERVER)); + require(5, mpegVideoCodec.CoCreateInstance(CLSID_CMpegVideoCodec, nullptr, CLSCTX_INPROC_SERVER)); + //require(4, mpegSplitter.CoCreateInstance(CLSID_MPEG1Splitter_Alt, nullptr, CLSCTX_INPROC_SERVER)); + //require(5, mpegVideoCodec.CoCreateInstance(CLSID_CMpegVideoCodec_Alt, nullptr, CLSCTX_INPROC_SERVER)); + require(6, mpegAudioCodec.CoCreateInstance(CLSID_CMpegAudioCodec, nullptr, CLSCTX_INPROC_SERVER)); + require(7, decodebin.CoCreateInstance(CLSID_decodebin_parser, nullptr, CLSCTX_INPROC_SERVER)); + require(8, dsound.CoCreateInstance(CLSID_DSoundRender, nullptr, CLSCTX_INPROC_SERVER)); + require(9, vmr.CoCreateInstance(CLSID_VideoMixingRenderer9, nullptr, CLSCTX_INPROC_SERVER)); + //require(9, vmr.CoCreateInstance(CLSID_VideoMixingRenderer, nullptr, CLSCTX_INPROC_SERVER)); + + require(13, filterGraph->AddFilter(asyncReader, L"asyncReader")); + //require(14, filterGraph->AddFilter(mpegSplitter, L"mpegSplitter")); + //require(15, filterGraph->AddFilter(mpegVideoCodec, L"mpegVideoCodec")); + //require(16, filterGraph->AddFilter(mpegAudioCodec, L"mpegAudioCodec")); + require(17, filterGraph->AddFilter(decodebin, L"decodebin")); + require(18, filterGraph->AddFilter(dsound, L"dsound")); + require(19, filterGraph->AddFilter(vmr, L"vmr")); + + require(20, asyncReader.QueryInterface(&asyncReaderFsf)); + require(21, asyncReaderFsf->Load(L"waga.wmv", nullptr), "failed to open waga.wmv, does the file exist?"); + //require(21, asyncReaderFsf->Load(L"video.mpg", nullptr), "failed to open video.mpg, does the file exist?"); + //require(21, asyncReaderFsf->Load(L"data_video_sample_640x360.mpeg", nullptr), "failed to open data_video_sample_640x360.mpeg, does the file exist?"); + + //require(31, connect_filters(filterGraph, asyncReader, mpegSplitter)); + //require(32, connect_filters(filterGraph, mpegSplitter, mpegVideoCodec)); + //require(33, connect_filters(filterGraph, mpegSplitter, mpegAudioCodec)); + //require(34, connect_filters(filterGraph, mpegVideoCodec, sink)); + //require(35, connect_filters(filterGraph, mpegAudioCodec, dsound)); + + //require(31, connect_filters(filterGraph, asyncReader, decodebin)); + //require(32, connect_filters(filterGraph, decodebin, sink)); + //require(33, connect_filters(filterGraph, decodebin, dsound)); + + //require(31, connect_filters(filterGraph, asyncReader, vmr)); + //require(31, connect_filters(filterGraph, asyncReader, mpegSplitter)); + //require(32, connect_filters(filterGraph, mpegSplitter, mpegVideoCodec)); + //require(34, connect_filters(filterGraph, mpegVideoCodec, vmr)); + //require(33, connect_filters(filterGraph, mpegSplitter, mpegAudioCodec)); + //require(35, connect_filters(filterGraph, mpegAudioCodec, dsound)); + + require(31, connect_filters(filterGraph, asyncReader, decodebin)); + require(32, connect_filters(filterGraph, decodebin, vmr)); + require(33, connect_filters(filterGraph, decodebin, dsound)); + */ + + puts("connected"); + + CComPtr filterGraph_mc; + require(40, filterGraph.QueryInterface(&filterGraph_mc)); + //Sleep(5000); + filterGraph_mc->Run(); + puts("running"); + + MSG msg; + BOOL bRet; + while( (bRet = GetMessage( &msg, NULL, 0, 0 )) != 0) + { + if (bRet == -1) + { + break; + // handle the error and possibly exit + } + else + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } + + puts("exit"); + exit(0); +} diff --git a/x/nanodecode.cpp b/x/nanodecode.cpp new file mode 100644 index 0000000..2cf59cc --- /dev/null +++ b/x/nanodecode.cpp @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: LGPL-2.0-or-later + +#define INITGUID +#define STRSAFE_NO_DEPRECATE +#include +#include +#include +#include +#include + +static const GUID GUID_NULL = {}; // not defined in my headers, how lovely +DEFINE_GUID(CLSID_decodebin_parser, 0xf9d8d64e, 0xa144, 0x47dc, 0x8e, 0xe0, 0xf5, 0x34, 0x98, 0x37, 0x2c, 0x29); +DEFINE_GUID(CLSID_MPEG1Splitter_Alt, 0x731537a0,0xda85,0x4c5b,0xa4,0xc3,0x45,0x6c,0x49,0x45,0xf8,0xfa); +DEFINE_GUID(CLSID_CMpegVideoCodec_Alt,0xe688d538,0xe607,0x4169,0x86,0xde,0xef,0x08,0x21,0xf5,0xe7,0xa7); + +static char* guid_to_str(const GUID& guid) +{ + static char buf[8][64]; + static int n = 0; + char* ret = buf[n++%8]; + sprintf(ret, "{%08lX-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}", guid.Data1, guid.Data2, guid.Data3, guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3], guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]); + return ret; +} +static wchar_t* guid_to_wstr(const GUID& guid) +{ + static wchar_t buf[8][64] = {}; + char* guids = guid_to_str(guid); + static int n = 0; + wchar_t* ret = buf[n++%8]; + for (int i=0;guids[i];i++) + ret[i] = guids[i]; + return ret; +} + +template class CComPtr { + void assign(T* ptr) + { + p = ptr; + } + void release() + { + if (p) + p->Release(); + p = nullptr; + } +public: + T* p; + + CComPtr() { p = nullptr; } + ~CComPtr() { release(); } + CComPtr(const CComPtr&) = delete; + CComPtr(CComPtr&&) = delete; + void operator=(const CComPtr&) = delete; + void operator=(CComPtr&&) = delete; + + CComPtr& operator=(T* ptr) + { + release(); + assign(ptr); + return *this; + } + T** operator&() + { + release(); + return &p; + } + T* operator->() { return p; } + operator T*() { return p; } + + HRESULT CoCreateInstance(REFCLSID rclsid, LPUNKNOWN pUnkOuter, DWORD dwClsContext) + { + release(); + return ::CoCreateInstance(rclsid, pUnkOuter, dwClsContext, IID_PPV_ARGS(&p)); + } + template + HRESULT QueryInterface(T2** other) + { + return p->QueryInterface(IID_PPV_ARGS(other)); + } +}; + +static HRESULT connect_filters(IGraphBuilder* graph, IBaseFilter* src, IBaseFilter* dst) +{ +puts("connect."); + CComPtr src_enum; + if (FAILED(src->EnumPins(&src_enum))) + return E_FAIL; + + CComPtr src_pin; + while (src_enum->Next(1, &src_pin, nullptr) == S_OK) + { +puts("src."); + PIN_INFO src_info; + if (FAILED(src_pin->QueryPinInfo(&src_info))) + return E_FAIL; + if (src_info.pFilter) + src_info.pFilter->Release(); + if (src_info.dir != PINDIR_OUTPUT) + continue; + + CComPtr check_pin; + src_pin->ConnectedTo(&check_pin); + if (check_pin != nullptr) + continue; + + CComPtr dst_enum; + dst->EnumPins(&dst_enum); + CComPtr dst_pin; + while (dst_enum->Next(1, &dst_pin, nullptr) == S_OK) + { +puts("dst."); + PIN_INFO dst_info; + if (FAILED(dst_pin->QueryPinInfo(&dst_info))) + return E_FAIL; + if (dst_info.pFilter) + dst_info.pFilter->Release(); + if (dst_info.dir != PINDIR_INPUT) + continue; + + dst_pin->ConnectedTo(&check_pin); + if (check_pin != nullptr) + continue; + +puts("match."); + //if (SUCCEEDED(graph->Connect(src_pin, dst_pin))) + if (SUCCEEDED(graph->ConnectDirect(src_pin, dst_pin, nullptr))) + { +puts("match2."); + return S_OK; + } +puts("match3."); + } + } + return E_FAIL; +} + +static void require(int seq, HRESULT hr, const char * text = "") +{ +printf("%d.\n", seq); + if (SUCCEEDED(hr)) + return; + + printf("fail: %d %.8lX%s%s\n", seq, hr, text?" ":"", text); + exit(seq); +} + + + +static CComPtr filterGraph; + +IBaseFilter* try_make_filter(REFIID riid) +{ + IBaseFilter* filt = nullptr; + CoCreateInstance(riid, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&filt)); + if (!filt) + return nullptr; + filterGraph->AddFilter(filt, guid_to_wstr(riid)); + return filt; +} +IBaseFilter* make_filter(IBaseFilter* filt) { return filt; } +IBaseFilter* make_filter(REFIID riid) +{ + IBaseFilter* filt = try_make_filter(riid); + if (!filt) + { + printf("failed to create %s\n", guid_to_str(riid)); + exit(1); + } + return filt; +} +bool connect_chain(IBaseFilter** filts, size_t n, bool required) +{ + for (size_t i=0;iQueryFilterInfo(&inf1); + second->QueryFilterInfo(&inf2); + printf("failed to connect %ls to %ls\n", inf1.achName, inf2.achName); + exit(1); + } + } + return true; +} +template +IBaseFilter* chain_tail(Ts... args) +{ + IBaseFilter* filts[] = { make_filter(args)... }; + connect_chain(filts, sizeof...(Ts), true); + return filts[sizeof...(Ts)-1]; +} +template +void chain(Ts... args) +{ + IBaseFilter* filts[] = { make_filter(args)... }; + connect_chain(filts, sizeof...(Ts), true); +} +template +bool try_chain(Ts... args) +{ + IBaseFilter* filts[] = { make_filter(args)... }; + return connect_chain(filts, sizeof...(Ts), false); +} + + +int main() +{ + setvbuf(stdout, nullptr, _IONBF, 0); + CoInitializeEx(NULL, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE); + + filterGraph.CoCreateInstance(CLSID_FilterGraph, nullptr, CLSCTX_INPROC_SERVER); + + IBaseFilter* asyncReader = make_filter(CLSID_AsyncReader); + CComPtr asyncReaderFsf; + require(20, asyncReader->QueryInterface(&asyncReaderFsf)); + require(21, asyncReaderFsf->Load(L"video.mpg", nullptr), "failed to open video.mpg, does the file exist?"); + + IBaseFilter* mpegdec = try_make_filter(CLSID_CMpegVideoCodec); + if (mpegdec) + { + IBaseFilter* demux = chain_tail(asyncReader, CLSID_MPEG1Splitter); + chain(demux, mpegdec, CLSID_VideoMixingRenderer9); + try_chain(demux, CLSID_CMpegAudioCodec, CLSID_DSoundRender); // don't worry too much if the file doesn't have sound + } + else + { + puts("CLSID_CMpegVideoCodec not available? Probably running on Wine, trying CLSID_decodebin_parser instead"); + IBaseFilter* demux = chain_tail(asyncReader, CLSID_decodebin_parser); + chain(demux, CLSID_VideoMixingRenderer9); + try_chain(demux, CLSID_DSoundRender); + } + + puts("connected"); + + CComPtr filterGraph_mc; + require(40, filterGraph.QueryInterface(&filterGraph_mc)); + filterGraph_mc->Run(); + puts("running"); + SleepEx(15000, true); + + puts("exit"); +} diff --git a/x/plmpeg_wine_dmo.cpp b/x/plmpeg_wine_dmo.cpp new file mode 100644 index 0000000..9ba4142 --- /dev/null +++ b/x/plmpeg_wine_dmo.cpp @@ -0,0 +1,784 @@ +// SPDX-License-Identifier: LGPL-2.0-or-later + +#ifdef __MINGW32__ +# define _FILE_OFFSET_BITS 64 +// mingw *really* wants to define its own printf/scanf, which adds ~20KB random stuff to the binary +// (on some 32bit mingw versions, it also adds a dependency on libgcc_s_sjlj-1.dll) +// extra kilobytes and dlls is the opposite of what I want, and my want is stronger, so here's some shenanigans +// comments say libstdc++ demands a POSIX printf, but I don't use libstdc++'s text functions, so I don't care +# define __USE_MINGW_ANSI_STDIO 0 // trigger a warning if it's enabled already - probably wrong include order +# include // include some random c++ header; they all include , +# undef __USE_MINGW_ANSI_STDIO // which ignores my #define above and sets this flag; re-clear it before including +# define __USE_MINGW_ANSI_STDIO 0 // (subsequent includes of c++config.h are harmless, there's an include guard) +#endif + +#define INITGUID +#define STRSAFE_NO_DEPRECATE +#include +#include +#include +#include +#include +#include +#include +#define PL_MPEG_IMPLEMENTATION +#include "pl_mpeg.h" + +static const GUID GUID_NULL = {}; // not defined in my headers, how lovely +DEFINE_GUID(IID_IUnknown, 0x00000000, 0x0000, 0x0000, 0xc0,0x00, 0x00,0x00,0x00,0x00,0x00,0x46); + + +// some of these aren't defined in my headers, or not where I want them +//DEFINE_GUID(MEDIASUBTYPE_I420,0x30323449,0x0000,0x0010,0x80,0x00,0x00,0xaa,0x00,0x38,0x9b,0x71); + + +// private guids, only meaningful in this dll +// there is a CLSID_MPEG1Splitter in Wine, but it's incomplete - it only supports audio, not video +DEFINE_GUID(CLSID_MPEG1Splitter_PlmpegDmo,0xd3d07daf,0x8ba2,0x49fd,0x9f,0xb4,0x47,0x8a,0xa2,0xe4,0x80,0xb1); +DEFINE_GUID(CLSID_CMpegVideoCodec_PlmpegDmo,0x64170df4,0x31c6,0x44e1,0x8d,0xfe,0xee,0x8e,0x59,0x13,0x04,0xcd); + + +#ifdef __i386__ +// needs some extra shenanigans to kill the stdcall @12 suffix +#define EXPORT(ret, name, args) \ + __asm__(".section .drectve; .ascii \" -export:" #name "\"; .text"); \ + extern "C" __stdcall ret name args __asm__("_" #name); \ + extern "C" __stdcall ret name args +#else +#define EXPORT(ret, name, args) \ + extern "C" __attribute__((__visibility__("default"))) __stdcall ret name args; \ + extern "C" __attribute__((__visibility__("default"))) __stdcall ret name args +#endif + + +static char* guid_to_str(const GUID& guid) +{ + static char buf[8][64]; + static int n = 0; + char* ret = buf[n++%8]; + sprintf(ret, "{%08lX-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}", guid.Data1, guid.Data2, guid.Data3, guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3], guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]); + return ret; +} + +template T min(T a, T b) { return a < b ? a : b; } +template T max(T a, T b) { return a > b ? a : b; } + + +template class CComPtr { + void assign(T* ptr) + { + p = ptr; + } + void release() + { + if (p) + p->Release(); + p = nullptr; + } +public: + T* p; + + CComPtr() { p = nullptr; } + ~CComPtr() { release(); } + CComPtr(const CComPtr&) = delete; + CComPtr(CComPtr&&) = delete; + void operator=(const CComPtr&) = delete; + void operator=(CComPtr&&) = delete; + + CComPtr& operator=(T* ptr) + { + release(); + assign(ptr); + return *this; + } + T** operator&() + { + release(); + return &p; + } + T* operator->() { return p; } + operator T*() { return p; } + + HRESULT CoCreateInstance(REFCLSID rclsid, LPUNKNOWN pUnkOuter, DWORD dwClsContext) + { + release(); + return ::CoCreateInstance(rclsid, pUnkOuter, dwClsContext, IID_PPV_ARGS(&p)); + } + template + HRESULT QueryInterface(T2** other) + { + return p->QueryInterface(IID_PPV_ARGS(other)); + } +}; + +template +class com_base_embedded : public Tis... { +private: + template T first_helper(); + template bool QueryInterfaceSingle(REFIID riid, void** ppvObject) + { + if (riid == __uuidof(Ti)) + { + Ti* ret = this; + ret->AddRef(); + *ppvObject = (void*)ret; + return true; + } + else return false; + } +public: + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override + { +printf("plmpeg QI %s\n", guid_to_str(riid)); + *ppvObject = nullptr; + if (riid == __uuidof(IUnknown)) + { + IUnknown* ret = (decltype(first_helper())*)this; + ret->AddRef(); + *ppvObject = (void*)ret; + return S_OK; + } + return (QueryInterfaceSingle(riid, ppvObject) || ...) ? S_OK : E_NOINTERFACE; + } +}; + +template +class com_base : public com_base_embedded { + uint32_t refcount = 1; +public: + ULONG STDMETHODCALLTYPE AddRef() override { return ++refcount; } + ULONG STDMETHODCALLTYPE Release() override + { + uint32_t new_refcount = --refcount; + if (!new_refcount) + delete this; + return new_refcount; + } +}; + +template +class com_base_aggregate : public Tis... { + // Microsoft docs imply that the outer object should have the local object's refcount and + // the inner object should implement all applicable interfaces, but that just sounds like a pain. + // (this kinda contradicts the rule that x->QI(IUnknown) == x->QI(IOuter)->QI(IUnknown), doesn't it? + // But only the parent object can access the secret IUnknown anyways.) +private: + class real_iunk : public IUnknown { + uint32_t refcount = 1; + com_base_aggregate* parent() { return container_of<&com_base_aggregate::iunk>(this); } + public: + ULONG STDMETHODCALLTYPE AddRef() override { return ++refcount; } + ULONG STDMETHODCALLTYPE Release() override + { + uint32_t new_refcount = --refcount; + if (!new_refcount) + delete parent(); + return new_refcount; + } + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override + { + return parent()->NonDelegatingQueryInterface(riid, ppvObject); + } + }; + + real_iunk iunk; + IUnknown* outer; + + template bool QueryInterfaceSingle(REFIID riid, void** ppvObject) + { + if (riid == __uuidof(Ti)) + { + Ti* ret = this; + ret->AddRef(); + *ppvObject = (void*)ret; + return true; + } + else return false; + } + HRESULT NonDelegatingQueryInterface(REFIID riid, void** ppvObject) + { +printf("plmpeg NDQI %s\n", guid_to_str(riid)); + if (riid == __uuidof(IUnknown)) + { + iunk.AddRef(); + *ppvObject = (void*)(IUnknown*)&iunk; + return S_OK; + } + *ppvObject = nullptr; + return (QueryInterfaceSingle(riid, ppvObject) || ...) ? S_OK : E_NOINTERFACE; + } + +public: + com_base_aggregate(IUnknown* pUnkOuter) : outer(pUnkOuter) {} + + IUnknown* real_iunknown() { return &iunk; } + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override + { +printf("plmpeg DQI %s\n", guid_to_str(riid)); + if (riid == __uuidof(IUnknown)) + { + iunk.AddRef(); + *ppvObject = (void*)&iunk; + return S_OK; + } + return outer->QueryInterface(riid, ppvObject); + } + + ULONG STDMETHODCALLTYPE AddRef() override { return outer->AddRef(); } + ULONG STDMETHODCALLTYPE Release() override { return outer->Release(); } +}; + +template +Tinner com_enum_helper(HRESULT STDMETHODCALLTYPE (Touter::*)(ULONG, Tinner**, ULONG*)); + +// don't inline this lambda into the template's default argument, gcc bug 105667 +static const auto addref_ptr = [](T* obj) { obj->AddRef(); return obj; }; +template +class com_enum : public com_base { + using Tret = decltype(com_enum_helper(&Timpl::Next)); + + Tret** items; + size_t pos = 0; + size_t len; + + com_enum(Tret** items, size_t pos, size_t len) : items(items), pos(pos), len(len) {} +public: + com_enum(Tret** items, size_t len) : items(items), len(len) {} + + HRESULT STDMETHODCALLTYPE Clone(Timpl** ppEnum) override + { + *ppEnum = new com_enum(items, pos, len); + return S_OK; + } + HRESULT STDMETHODCALLTYPE Next(ULONG celt, Tret** rgelt, ULONG* pceltFetched) override + { + size_t remaining = len - pos; + size_t ret = min((size_t)celt, remaining); + for (size_t n=0;n= celt) + return S_OK; + else + return S_FALSE; + } + HRESULT STDMETHODCALLTYPE Reset() override + { + pos = 0; + return S_OK; + } + HRESULT STDMETHODCALLTYPE Skip(ULONG celt) override + { + size_t remaining = len - pos; + if (remaining >= celt) + { + pos += celt; + return S_OK; + } + else + { + pos = len; + return S_FALSE; + } + } +}; + + +// inspired by the Linux kernel macro, but using a member pointer looks cleaner; non-expressions (like member names) in macros look wrong +template Tc* container_of(Ti* ptr, Ti Tc:: * memb) +{ + // https://wg21.link/P0908 proposes a better implementation, but it was forgotten and not accepted + Tc* fake_object = (Tc*)0x12345678; // doing math on a fake pointer is UB, but good luck proving it's bogus + size_t offset = (uintptr_t)&(fake_object->*memb) - (uintptr_t)fake_object; + return (Tc*)((uint8_t*)ptr - offset); +} +template const Tc* container_of(const Ti* ptr, Ti Tc:: * memb) +{ + return container_of((Ti*)ptr, memb); +} +template auto container_of(Ti* ptr) { return container_of(ptr, memb); } + + +HRESULT qi_release(IUnknown* obj, REFIID riid, void** ppvObj) +{ + HRESULT hr = obj->QueryInterface(riid, ppvObj); + obj->Release(); + return hr; +} + + +static HRESULT CopyMediaType(AM_MEDIA_TYPE * pmtTarget, const AM_MEDIA_TYPE * pmtSource) +{ + *pmtTarget = *pmtSource; + if (pmtSource->pbFormat != nullptr) + { + pmtTarget->pbFormat = (uint8_t*)CoTaskMemAlloc(pmtSource->cbFormat); + memcpy(pmtTarget->pbFormat, pmtSource->pbFormat, pmtSource->cbFormat); + } + return S_OK; +} + +static AM_MEDIA_TYPE* CreateMediaType(const AM_MEDIA_TYPE * pSrc) +{ + AM_MEDIA_TYPE* ret = (AM_MEDIA_TYPE*)CoTaskMemAlloc(sizeof(AM_MEDIA_TYPE)); + CopyMediaType(ret, pSrc); + return ret; +} + + +class scoped_lock { + SRWLOCK* lock; +public: + scoped_lock(SRWLOCK* lock) : lock(lock) { AcquireSRWLockExclusive(lock); } + ~scoped_lock() { ReleaseSRWLockExclusive(lock); } +}; +class scoped_unlock { + SRWLOCK* lock; +public: + scoped_unlock(SRWLOCK* lock) : lock(lock) { ReleaseSRWLockExclusive(lock); } + ~scoped_unlock() { AcquireSRWLockExclusive(lock); } +}; + +template +class base_dmo : public com_base_aggregate { + Touter* parent() { return (Touter*)this; } + +public: + + void debug(const char * fmt, ...) + { + char buf[1024]; + + va_list args; + va_start(args, fmt); + vsnprintf(buf, 1024, fmt, args); + va_end(args); + + fprintf(stdout, "%s %s\n", typeid(Touter).name(), buf); + fflush(stdout); + } + + using com_base_aggregate::com_base_aggregate; + + HRESULT STDMETHODCALLTYPE AllocateStreamingResources() override + { + debug("IMediaObject AllocateStreamingResources"); + return E_OUTOFMEMORY; + } + + HRESULT STDMETHODCALLTYPE Discontinuity(DWORD dwInputStreamIndex) override + { + debug("IMediaObject Discontinuity"); + return E_OUTOFMEMORY; + } + + HRESULT STDMETHODCALLTYPE Flush() override + { + debug("IMediaObject Flush"); + return E_OUTOFMEMORY; + } + + HRESULT STDMETHODCALLTYPE FreeStreamingResources() override + { + debug("IMediaObject FreeStreamingResources"); + return E_OUTOFMEMORY; + } + + HRESULT STDMETHODCALLTYPE GetInputCurrentType(DWORD dwInputStreamIndex, DMO_MEDIA_TYPE* pmt) override + { + debug("IMediaObject GetInputCurrentType"); + return E_OUTOFMEMORY; + } + + HRESULT STDMETHODCALLTYPE GetInputMaxLatency(DWORD dwInputStreamIndex, REFERENCE_TIME* prtMaxLatency) override + { + debug("IMediaObject GetInputMaxLatency"); + return E_OUTOFMEMORY; + } + + HRESULT STDMETHODCALLTYPE GetInputSizeInfo(DWORD dwInputStreamIndex, DWORD* pcbSize, DWORD* pcbMaxLookahead, DWORD* pcbAlignment) override + { + debug("IMediaObject GetInputSizeInfo"); + return E_OUTOFMEMORY; + } + + HRESULT STDMETHODCALLTYPE GetInputStatus(DWORD dwInputStreamIndex, DWORD* dwFlags) override + { + debug("IMediaObject GetInputStatus"); + return E_OUTOFMEMORY; + } + + HRESULT STDMETHODCALLTYPE GetInputStreamInfo(DWORD dwInputStreamIndex, DWORD* pdwFlags) override + { + debug("IMediaObject GetInputStreamInfo"); + return E_OUTOFMEMORY; + } + + HRESULT STDMETHODCALLTYPE GetInputType(DWORD dwInputStreamIndex, DWORD dwTypeIndex, DMO_MEDIA_TYPE* pmt) override + { + debug("IMediaObject GetInputType"); + return E_OUTOFMEMORY; + } + + HRESULT STDMETHODCALLTYPE GetOutputCurrentType(DWORD dwOutputStreamIndex, DMO_MEDIA_TYPE* pmt) override + { + debug("IMediaObject GetOutputCurrentType"); + return E_OUTOFMEMORY; + } + + HRESULT STDMETHODCALLTYPE GetOutputSizeInfo(DWORD dwOutputStreamIndex, DWORD* pcbSize, DWORD* pcbAlignment) override + { + debug("IMediaObject GetOutputSizeInfo"); + return E_OUTOFMEMORY; + } + + HRESULT STDMETHODCALLTYPE GetOutputStreamInfo(DWORD dwOutputStreamIndex, DWORD* pdwFlags) override + { + debug("IMediaObject GetOutputStreamInfo"); + return E_OUTOFMEMORY; + } + + HRESULT STDMETHODCALLTYPE GetOutputType(DWORD dwOutputStreamIndex, DWORD dwTypeIndex, DMO_MEDIA_TYPE* pmt) override + { + debug("IMediaObject GetOutputType"); + return E_OUTOFMEMORY; + } + + HRESULT STDMETHODCALLTYPE Lock(LONG bLock) override + { + debug("IMediaObject Lock"); + return E_OUTOFMEMORY; + } + + HRESULT STDMETHODCALLTYPE ProcessInput(DWORD dwInputStreamIndex, IMediaBuffer* pBuffer, DWORD dwFlags, + REFERENCE_TIME rtTimestamp, REFERENCE_TIME rtTimelength) override + { + debug("IMediaObject ProcessInput"); + return E_OUTOFMEMORY; + } + + HRESULT STDMETHODCALLTYPE ProcessOutput(DWORD dwFlags, DWORD cOutputBufferCount, + DMO_OUTPUT_DATA_BUFFER* pOutputBuffers, DWORD* pdwStatus) override + { + debug("IMediaObject ProcessOutput"); + return E_OUTOFMEMORY; + } + + HRESULT STDMETHODCALLTYPE SetInputMaxLatency(DWORD dwInputStreamIndex, REFERENCE_TIME rtMaxLatency) override + { + debug("IMediaObject SetInputMaxLatency"); + return E_OUTOFMEMORY; + } + + HRESULT STDMETHODCALLTYPE SetOutputType(DWORD dwOutputStreamIndex, const DMO_MEDIA_TYPE* pmt, DWORD dwFlags) override + { + debug("IMediaObject SetOutputType"); + return E_OUTOFMEMORY; + } +}; + + + + + +class CMPEG1Splitter : public base_dmo { +public: + using base_dmo::base_dmo; + + HRESULT STDMETHODCALLTYPE GetStreamCount(DWORD* pcInputStreams, DWORD* pcOutputStreams) override + { + debug("IMediaObject GetStreamCount"); + *pcInputStreams = 1; + *pcOutputStreams = 2; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE SetInputType(DWORD dwInputStreamIndex, const DMO_MEDIA_TYPE* pmt, DWORD dwFlags) override + { + debug("IMediaObject SetInputType %s %s %s", guid_to_str(pmt->majortype), guid_to_str(pmt->subtype), guid_to_str(pmt->formattype)); + if (pmt->majortype == MEDIATYPE_Stream && pmt->subtype == MEDIASUBTYPE_MPEG1System) + return S_OK; + return DMO_E_TYPE_NOT_ACCEPTED; + } + + HRESULT STDMETHODCALLTYPE GetOutputType(DWORD dwOutputStreamIndex, DWORD dwTypeIndex, DMO_MEDIA_TYPE* pmt) override + { + debug("IMediaObject GetOutputType %lu %lu", dwOutputStreamIndex, dwTypeIndex); + if (dwTypeIndex > 0) + return DMO_E_NO_MORE_ITEMS; + if (dwOutputStreamIndex == 0) + { + int width = 640; + int height = 480; + double fps = 60.0; + + MPEG1VIDEOINFO mediatype_head = { + .hdr = { + .rcSource = { 0, 0, width, height }, + .rcTarget = { 0, 0, width, height }, + .dwBitRate = 0, + .dwBitErrorRate = 0, + .AvgTimePerFrame = (REFERENCE_TIME)(10000000/fps), + .bmiHeader = { + .biSize = sizeof(BITMAPINFOHEADER), + .biWidth = width, + .biHeight = height, + .biPlanes = 0, + .biBitCount = 0, + .biCompression = 0, + .biSizeImage = 0, + }, + }, + .dwStartTimeCode = 0, + .cbSequenceHeader = 0, + .bSequenceHeader = {}, + }; + AM_MEDIA_TYPE media_type = { + .majortype = MEDIATYPE_Video, + .subtype = MEDIASUBTYPE_MPEG1Packet, + .bFixedSizeSamples = false, + .bTemporalCompression = true, + .lSampleSize = 0, + .formattype = FORMAT_MPEGVideo, + .pUnk = nullptr, + .cbFormat = sizeof(mediatype_head), + .pbFormat = (BYTE*)&mediatype_head, + }; + + CopyMediaType((AM_MEDIA_TYPE*)pmt, &media_type); + } + else + { + int rate = 48000; + + MPEG1WAVEFORMAT mediatype_head = { + .wfx = { + .wFormatTag = WAVE_FORMAT_MPEG, + .nChannels = 2, + .nSamplesPerSec = (DWORD)rate, + .nAvgBytesPerSec = 4000, + .nBlockAlign = 48, + .wBitsPerSample = 0, + .cbSize = sizeof(mediatype_head), + }, + .fwHeadLayer = ACM_MPEG_LAYER2, + .dwHeadBitrate = (DWORD)rate, + .fwHeadMode = ACM_MPEG_STEREO, + .fwHeadModeExt = 0, + .wHeadEmphasis = 0, + .fwHeadFlags = ACM_MPEG_ID_MPEG1, + .dwPTSLow = 0, + .dwPTSHigh = 0, + }; + AM_MEDIA_TYPE media_type = { + .majortype = MEDIATYPE_Audio, + .subtype = MEDIASUBTYPE_MPEG1AudioPayload, // wine understands only MPEG1AudioPayload, not MPEG1Packet or MPEG1Payload + .bFixedSizeSamples = false, + .bTemporalCompression = true, + .lSampleSize = 0, + .formattype = FORMAT_WaveFormatEx, + .pUnk = nullptr, + .cbFormat = sizeof(mediatype_head), + .pbFormat = (BYTE*)&mediatype_head, + }; + + CopyMediaType((AM_MEDIA_TYPE*)pmt, &media_type); + } + return S_OK; + } + + HRESULT STDMETHODCALLTYPE SetOutputType(DWORD dwOutputStreamIndex, const DMO_MEDIA_TYPE* pmt, DWORD dwFlags) override + { + debug("IMediaObject SetOutputType"); + if (pmt->formattype == FORMAT_MPEGVideo) + { + MPEG1VIDEOINFO* mt_body = (MPEG1VIDEOINFO*)pmt->pbFormat; + debug("video %d\n", mt_body->hdr.rcSource.bottom); + } + return S_OK; + } + + HRESULT STDMETHODCALLTYPE GetOutputSizeInfo(DWORD dwOutputStreamIndex, DWORD* pcbSize, DWORD* pcbAlignment) override + { + debug("IMediaObject GetOutputSizeInfo"); + if (dwOutputStreamIndex == 0) + { + // don't care, just pick some random numbers + *pcbSize = 8192; + *pcbAlignment = 1; + } + else + { + *pcbSize = 8192; + *pcbAlignment = 1; + } + return S_OK; + } +}; + +class CMpegVideoCodec : public base_dmo { +public: + using base_dmo::base_dmo; + + HRESULT STDMETHODCALLTYPE GetStreamCount(DWORD* pcInputStreams, DWORD* pcOutputStreams) override + { + debug("IMediaObject GetStreamCount"); + *pcInputStreams = 1; + *pcOutputStreams = 1; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE SetInputType(DWORD dwInputStreamIndex, const DMO_MEDIA_TYPE* pmt, DWORD dwFlags) override + { + debug("IMediaObject SetInputType"); + return E_OUTOFMEMORY; + } +}; + + + + + +template +class ClassFactory : public com_base { +public: + HRESULT STDMETHODCALLTYPE CreateInstance(IUnknown* pUnkOuter, REFIID riid, void** ppvObject) + { + if constexpr (aggregation) + { + if (pUnkOuter != nullptr && riid != IID_IUnknown) + return E_NOINTERFACE; + *ppvObject = (void*)(new T(pUnkOuter))->real_iunknown(); + return S_OK; + } + else + { + if (pUnkOuter != nullptr) + return CLASS_E_NOAGGREGATION; + return qi_release(new T(), riid, ppvObject); + } + } + HRESULT STDMETHODCALLTYPE LockServer(BOOL lock) { return S_OK; } // don't care +}; + +template +class DmoClassFactory : public com_base { +public: + HRESULT STDMETHODCALLTYPE CreateInstance(IUnknown* pUnkOuter, REFIID riid, void** ppvObject) + { + CComPtr dmo; + HRESULT hr = dmo.CoCreateInstance(CLSID_DMOWrapperFilter, pUnkOuter, CLSCTX_INPROC); + if (FAILED(hr)) + return hr; + hr = dmo->Init(*guid, DMOCATEGORY_VIDEO_DECODER); + if (FAILED(hr)) + return hr; + + return dmo->QueryInterface(riid, ppvObject); + } + HRESULT STDMETHODCALLTYPE LockServer(BOOL lock) { return S_OK; } // don't care +}; + +static bool initialized = false; +EXPORT(HRESULT, DllGetClassObject, (REFCLSID rclsid, REFIID riid, void** ppvObj)) +{ + if (!initialized) + { + setvbuf(stdout, nullptr, _IONBF, 0); + initialized = true; + DWORD dummy; + CoRegisterClassObject(CLSID_MPEG1Splitter_PlmpegDmo, new ClassFactory, + CLSCTX_INPROC_SERVER, REGCLS_MULTIPLEUSE, &dummy); + CoRegisterClassObject(CLSID_CMpegVideoCodec_PlmpegDmo, new ClassFactory, + CLSCTX_INPROC_SERVER, REGCLS_MULTIPLEUSE, &dummy); + } +fprintf(stdout, "DllGetClassObject %s\n", guid_to_str(rclsid)); fflush(stdout); +//CloseHandle(CreateFileA("Z:\\home\\walrus\\Desktop\\DllGetClassObject.txt", GENERIC_WRITE, 0, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)); + if (rclsid == CLSID_MPEG1Splitter) + return qi_release(new DmoClassFactory<&CLSID_MPEG1Splitter_PlmpegDmo>(), riid, ppvObj); + if (rclsid == CLSID_CMpegVideoCodec) + return qi_release(new DmoClassFactory<&CLSID_CMpegVideoCodec_PlmpegDmo>(), riid, ppvObj); + return CLASS_E_CLASSNOTAVAILABLE; +} + +EXPORT(HRESULT, DllCanUnloadNow, ()) +{ + return S_FALSE; // just don't bother +} + +/* + +[Software\\Classes\\CLSID\\{FEB50740-7BEF-11CE-9BD9-0000E202599C}] 1641316191 +#time=1d8018de2a218a4 +@="MPEG-I Video Decoder" + +[Software\\Classes\\CLSID\\{FEB50740-7BEF-11CE-9BD9-0000E202599C}\\InProcServer32] 1641316191 +#time=1d8018de2a21926 +@="Z:\\home\\walrus\\x\\plmpeg_gst\\plmpeg_wine.dll" +"ThreadingModel"="Both" + +[Software\\Classes\\CLSID\\{336475D0-942A-11CE-A870-00AA002FEAB5}] 1641316191 +#time=1d8018de2a218a4 +@="MPEG-I Stream Splitter" + +[Software\\Classes\\CLSID\\{336475D0-942A-11CE-A870-00AA002FEAB5}\\InProcServer32] 1641316191 +#time=1d8018de2a21926 +@="Z:\\home\\walrus\\x\\plmpeg_gst\\plmpeg_wine.dll" +"ThreadingModel"="Both" + +[Software\\Classes\\Wow6432Node\\CLSID\\{FEB50740-7BEF-11CE-9BD9-0000E202599C}] 1641316191 +#time=1d8018de2a218a4 +@="MPEG-I Video Decoder" + +[Software\\Classes\\Wow6432Node\\CLSID\\{FEB50740-7BEF-11CE-9BD9-0000E202599C}\\InProcServer32] 1641316191 +#time=1d8018de2a21926 +@="Z:\\home\\walrus\\x\\plmpeg_gst\\plmpeg_wine32.dll" +"ThreadingModel"="Both" + +[Software\\Classes\\Wow6432Node\\CLSID\\{336475D0-942A-11CE-A870-00AA002FEAB5}] 1641316191 +#time=1d8018de2a218a4 +@="MPEG-I Stream Splitter" + +[Software\\Classes\\Wow6432Node\\CLSID\\{336475D0-942A-11CE-A870-00AA002FEAB5}\\InProcServer32] 1641316191 +#time=1d8018de2a21926 +@="Z:\\home\\walrus\\x\\plmpeg_gst\\plmpeg_wine32.dll" +"ThreadingModel"="Both" + + + + + +original + + +[Software\\Classes\\CLSID\\{336475D0-942A-11CE-A870-00AA002FEAB5}] 1641316191 +#time=1d8018de2a218a4 +@="MPEG-I Stream Splitter" + +[Software\\Classes\\CLSID\\{336475D0-942A-11CE-A870-00AA002FEAB5}\\InProcServer32] 1641316191 +#time=1d8018de2a21926 +@="C:\\windows\\system32\\winegstreamer.dll" +"ThreadingModel"="Both" + +[Software\\Classes\\Wow6432Node\\CLSID\\{336475D0-942A-11CE-A870-00AA002FEAB5}] 1641316197 +#time=1d8018de622ae30 +@="MPEG-I Stream Splitter" + +[Software\\Classes\\Wow6432Node\\CLSID\\{336475D0-942A-11CE-A870-00AA002FEAB5}\\InProcServer32] 1641316197 +#time=1d8018de622ae80 +@="C:\\windows\\system32\\winegstreamer.dll" +"ThreadingModel"="Both" + + +*/ + +#ifdef __MINGW32__ +// deleting these things removes a few kilobytes of binary and a dependency on libstdc++-6.dll +void* operator new(std::size_t n) _GLIBCXX_THROW(std::bad_alloc) { return malloc(n); } +void operator delete(void* p) noexcept { free(p); } +void operator delete(void* p, std::size_t n) noexcept { operator delete(p); } +extern "C" void __cxa_pure_virtual() { __builtin_trap(); } +extern "C" void _pei386_runtime_relocator() {} +#endif