shenjack
f9eeafe322
readme update 看起来更像 Dear ImGui 一些(looks more like Dear ImGui and some intersting feature to the button remove debug 确认一下action 404 修改 writing theme looks good better? a ? alpha=255 not 256 looks better try new pyglet first 看起来好一些 sync pyglet 水一手 这波必须得水一手了,要不然commit太少了(确信 丢点正经东西上去 顺手继承一下Options 补充docs 坏了,忘记水commit了( 至少我能早睡了( 这里还能水一点来着( 试试再说 reee 保证能跑( 同步lib not dr 的修改 忘记带上 None了 还是加上一个额外的判断参数吧 刷点commit也不错 先更新一下依赖版本 水commit啦 理论可行,实践开始! 构建参数喜加一 reeeee 更新一下 pyproject 的依赖 fix typing looks better 水一个( 测试? sync pyglet to master A | Try use rust-cache looks good? what? C | sync pyglet A | 添加了一个 Button Draw Theme A | Magic Number (确信) A | 尽量不继承Options sync pyglet A | Add theme A | Add more Theme information Enhance | Theme sync pyglet Add | add unifont Enhance | use os.walk in font loading Enhance | Use i18n in font loading Enhance | doc sync pyglet Add | add 3.12 build option to build_rs Fix | Button position have a z sync pyglet A | Logger.py 启动! sync pyglet Changed | Bump pyo3 to 0.20.0 add logger.py update logger! Add | more logger! Add | lib-not-dr some lib-not-dr
847 lines
33 KiB
Python
847 lines
33 KiB
Python
import os
|
|
import platform
|
|
import warnings
|
|
|
|
from pyglet import image
|
|
from pyglet.libs.win32 import _kernel32 as kernel32
|
|
from pyglet.libs.win32 import _ole32 as ole32
|
|
from pyglet.libs.win32 import com
|
|
from pyglet.libs.win32.constants import *
|
|
from pyglet.libs.win32.types import *
|
|
from pyglet.media import Source
|
|
from pyglet.media.codecs import AudioFormat, AudioData, VideoFormat, MediaDecoder, StaticSource
|
|
from pyglet.util import debug_print, DecodeException
|
|
|
|
_debug = debug_print('debug_media')
|
|
|
|
try:
|
|
mfreadwrite = 'mfreadwrite'
|
|
mfplat = 'mfplat'
|
|
|
|
# System32 and SysWOW64 folders are opposite perception in Windows x64.
|
|
# System32 = x64 dll's | SysWOW64 = x86 dlls
|
|
# By default ctypes only seems to look in system32 regardless of Python architecture, which has x64 dlls.
|
|
if platform.architecture()[0] == '32bit':
|
|
if platform.machine().endswith('64'): # Machine is 64 bit, Python is 32 bit.
|
|
mfreadwrite = os.path.join(os.environ['WINDIR'], 'SysWOW64', 'mfreadwrite.dll')
|
|
mfplat = os.path.join(os.environ['WINDIR'], 'SysWOW64', 'mfplat.dll')
|
|
|
|
mfreadwrite_lib = ctypes.windll.LoadLibrary(mfreadwrite)
|
|
mfplat_lib = ctypes.windll.LoadLibrary(mfplat)
|
|
except OSError:
|
|
# Doesn't exist? Should stop import of library.
|
|
raise ImportError('Could not load WMF library.')
|
|
|
|
MF_SOURCE_READERF_ERROR = 0x00000001
|
|
MF_SOURCE_READERF_ENDOFSTREAM = 0x00000002
|
|
MF_SOURCE_READERF_NEWSTREAM = 0x00000004
|
|
MF_SOURCE_READERF_NATIVEMEDIATYPECHANGED = 0x00000010
|
|
MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED = 0x00000020
|
|
MF_SOURCE_READERF_STREAMTICK = 0x00000100
|
|
|
|
# Audio attributes
|
|
MF_LOW_LATENCY = com.GUID(0x9c27891a, 0xed7a, 0x40e1, 0x88, 0xe8, 0xb2, 0x27, 0x27, 0xa0, 0x24, 0xee)
|
|
|
|
# Audio information
|
|
MF_MT_ALL_SAMPLES_INDEPENDENT = com.GUID(0xc9173739, 0x5e56, 0x461c, 0xb7, 0x13, 0x46, 0xfb, 0x99, 0x5c, 0xb9, 0x5f)
|
|
MF_MT_FIXED_SIZE_SAMPLES = com.GUID(0xb8ebefaf, 0xb718, 0x4e04, 0xb0, 0xa9, 0x11, 0x67, 0x75, 0xe3, 0x32, 0x1b)
|
|
MF_MT_SAMPLE_SIZE = com.GUID(0xdad3ab78, 0x1990, 0x408b, 0xbc, 0xe2, 0xeb, 0xa6, 0x73, 0xda, 0xcc, 0x10)
|
|
MF_MT_COMPRESSED = com.GUID(0x3afd0cee, 0x18f2, 0x4ba5, 0xa1, 0x10, 0x8b, 0xea, 0x50, 0x2e, 0x1f, 0x92)
|
|
MF_MT_WRAPPED_TYPE = com.GUID(0x4d3f7b23, 0xd02f, 0x4e6c, 0x9b, 0xee, 0xe4, 0xbf, 0x2c, 0x6c, 0x69, 0x5d)
|
|
MF_MT_AUDIO_NUM_CHANNELS = com.GUID(0x37e48bf5, 0x645e, 0x4c5b, 0x89, 0xde, 0xad, 0xa9, 0xe2, 0x9b, 0x69, 0x6a)
|
|
MF_MT_AUDIO_SAMPLES_PER_SECOND = com.GUID(0x5faeeae7, 0x0290, 0x4c31, 0x9e, 0x8a, 0xc5, 0x34, 0xf6, 0x8d, 0x9d, 0xba)
|
|
MF_MT_AUDIO_FLOAT_SAMPLES_PER_SECOND = com.GUID(0xfb3b724a, 0xcfb5, 0x4319, 0xae, 0xfe, 0x6e, 0x42, 0xb2, 0x40, 0x61, 0x32)
|
|
MF_MT_AUDIO_AVG_BYTES_PER_SECOND = com.GUID(0x1aab75c8, 0xcfef, 0x451c, 0xab, 0x95, 0xac, 0x03, 0x4b, 0x8e, 0x17, 0x31)
|
|
MF_MT_AUDIO_BLOCK_ALIGNMENT = com.GUID(0x322de230, 0x9eeb, 0x43bd, 0xab, 0x7a, 0xff, 0x41, 0x22, 0x51, 0x54, 0x1d)
|
|
MF_MT_AUDIO_BITS_PER_SAMPLE = com.GUID(0xf2deb57f, 0x40fa, 0x4764, 0xaa, 0x33, 0xed, 0x4f, 0x2d, 0x1f, 0xf6, 0x69)
|
|
MF_MT_AUDIO_VALID_BITS_PER_SAMPLE = com.GUID(0xd9bf8d6a, 0x9530, 0x4b7c, 0x9d, 0xdf, 0xff, 0x6f, 0xd5, 0x8b, 0xbd, 0x06)
|
|
MF_MT_AUDIO_SAMPLES_PER_BLOCK = com.GUID(0xaab15aac, 0xe13a, 0x4995, 0x92, 0x22, 0x50, 0x1e, 0xa1, 0x5c, 0x68, 0x77)
|
|
MF_MT_AUDIO_CHANNEL_MASK = com.GUID(0x55fb5765, 0x644a, 0x4caf, 0x84, 0x79, 0x93, 0x89, 0x83, 0xbb, 0x15, 0x88)
|
|
MF_PD_DURATION = com.GUID(0x6c990d33, 0xbb8e, 0x477a, 0x85, 0x98, 0xd, 0x5d, 0x96, 0xfc, 0xd8, 0x8a)
|
|
|
|
|
|
# Media types categories
|
|
MF_MT_MAJOR_TYPE = com.GUID(0x48eba18e, 0xf8c9, 0x4687, 0xbf, 0x11, 0x0a, 0x74, 0xc9, 0xf9, 0x6a, 0x8f)
|
|
MF_MT_SUBTYPE = com.GUID(0xf7e34c9a, 0x42e8, 0x4714, 0xb7, 0x4b, 0xcb, 0x29, 0xd7, 0x2c, 0x35, 0xe5)
|
|
|
|
# Major types
|
|
MFMediaType_Audio = com.GUID(0x73647561, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71)
|
|
MFMediaType_Video = com.GUID(0x73646976, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71)
|
|
MFMediaType_Protected = com.GUID(0x7b4b6fe6, 0x9d04, 0x4494, 0xbe, 0x14, 0x7e, 0x0b, 0xd0, 0x76, 0xc8, 0xe4)
|
|
MFMediaType_Image = com.GUID(0x72178C23, 0xE45B, 0x11D5, 0xBC, 0x2A, 0x00, 0xB0, 0xD0, 0xF3, 0xF4, 0xAB)
|
|
MFMediaType_HTML = com.GUID(0x72178C24, 0xE45B, 0x11D5, 0xBC, 0x2A, 0x00, 0xB0, 0xD0, 0xF3, 0xF4, 0xAB)
|
|
MFMediaType_Subtitle = com.GUID(0xa6d13581, 0xed50, 0x4e65, 0xae, 0x08, 0x26, 0x06, 0x55, 0x76, 0xaa, 0xcc)
|
|
|
|
# Video subtypes, attributes, and enums (Uncompressed)
|
|
D3DFMT_X8R8G8B8 = 22
|
|
D3DFMT_P8 = 41
|
|
D3DFMT_A8R8G8B8 = 21
|
|
MFVideoFormat_RGB32 = com.GUID(D3DFMT_X8R8G8B8, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71)
|
|
MFVideoFormat_RGB8 = com.GUID(D3DFMT_P8, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71)
|
|
MFVideoFormat_ARGB32 = com.GUID(D3DFMT_A8R8G8B8, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71)
|
|
|
|
MFVideoInterlace_Progressive = 2
|
|
MF_MT_INTERLACE_MODE = com.GUID(0xe2724bb8, 0xe676, 0x4806, 0xb4, 0xb2, 0xa8, 0xd6, 0xef, 0xb4, 0x4c, 0xcd)
|
|
MF_MT_FRAME_SIZE = com.GUID(0x1652c33d, 0xd6b2, 0x4012, 0xb8, 0x34, 0x72, 0x03, 0x08, 0x49, 0xa3, 0x7d)
|
|
MF_MT_FRAME_RATE = com.GUID(0xc459a2e8, 0x3d2c, 0x4e44, 0xb1, 0x32, 0xfe, 0xe5, 0x15, 0x6c, 0x7b, 0xb0)
|
|
MF_MT_PIXEL_ASPECT_RATIO = com.GUID(0xc6376a1e, 0x8d0a, 0x4027, 0xbe, 0x45, 0x6d, 0x9a, 0x0a, 0xd3, 0x9b, 0xb6)
|
|
MF_MT_DRM_FLAGS = com.GUID(0x8772f323, 0x355a, 0x4cc7, 0xbb, 0x78, 0x6d, 0x61, 0xa0, 0x48, 0xae, 0x82)
|
|
MF_MT_DEFAULT_STRIDE = com.GUID(0x644b4e48, 0x1e02, 0x4516, 0xb0, 0xeb, 0xc0, 0x1c, 0xa9, 0xd4, 0x9a, 0xc6)
|
|
|
|
# Audio Subtypes (Uncompressed)
|
|
WAVE_FORMAT_PCM = 1
|
|
WAVE_FORMAT_IEEE_FLOAT = 3
|
|
MFAudioFormat_PCM = com.GUID(WAVE_FORMAT_PCM, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71)
|
|
MFAudioFormat_Float = com.GUID(WAVE_FORMAT_IEEE_FLOAT, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71)
|
|
|
|
# Image subtypes.
|
|
MFImageFormat_RGB32 = com.GUID(0x00000016, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71)
|
|
MFImageFormat_JPEG = com.GUID(0x19e4a5aa, 0x5662, 0x4fc5, 0xa0, 0xc0, 0x17, 0x58, 0x02, 0x8e, 0x10, 0x57)
|
|
|
|
# Video attributes
|
|
# Enables hardware decoding
|
|
MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS = com.GUID(0xa634a91c, 0x822b, 0x41b9, 0xa4, 0x94, 0x4d, 0xe4, 0x64, 0x36, 0x12,
|
|
0xb0)
|
|
# Enable video decoding
|
|
MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = com.GUID(0xfb394f3d, 0xccf1, 0x42ee, 0xbb, 0xb3, 0xf9, 0xb8, 0x45, 0xd5,
|
|
0x68, 0x1d)
|
|
MF_SOURCE_READER_D3D_MANAGER = com.GUID(0xec822da2, 0xe1e9, 0x4b29, 0xa0, 0xd8, 0x56, 0x3c, 0x71, 0x9f, 0x52, 0x69)
|
|
MF_MEDIA_ENGINE_DXGI_MANAGER = com.GUID(0x065702da, 0x1094, 0x486d, 0x86, 0x17, 0xee, 0x7c, 0xc4, 0xee, 0x46, 0x48)
|
|
MF_SOURCE_READER_ENABLE_ADVANCED_VIDEO_PROCESSING = com.GUID(0xf81da2c, 0xb537, 0x4672, 0xa8, 0xb2, 0xa6, 0x81, 0xb1,
|
|
0x73, 0x7, 0xa3)
|
|
|
|
# Some common errors
|
|
MF_E_INVALIDSTREAMNUMBER = -1072875853 # 0xC00D36B3
|
|
MF_E_UNSUPPORTED_BYTESTREAM_TYPE = -1072875836 # 0xC00D36C4
|
|
MF_E_NO_MORE_TYPES = 0xC00D36B9
|
|
MF_E_TOPO_CODEC_NOT_FOUND = -1072868846 # 0xC00D5212
|
|
|
|
|
|
VT_I8 = 20 # Only enum we care about: https://docs.microsoft.com/en-us/windows/win32/api/wtypes/ne-wtypes-varenum
|
|
|
|
|
|
def timestamp_from_wmf(timestamp): # 100-nanoseconds
|
|
return float(timestamp) / 10000000
|
|
|
|
|
|
def timestamp_to_wmf(timestamp): # 100-nanoseconds
|
|
return int(timestamp * 10000000)
|
|
|
|
|
|
class IMFAttributes(com.pIUnknown):
|
|
_methods_ = [
|
|
('GetItem',
|
|
com.STDMETHOD()),
|
|
('GetItemType',
|
|
com.STDMETHOD()),
|
|
('CompareItem',
|
|
com.STDMETHOD()),
|
|
('Compare',
|
|
com.STDMETHOD()),
|
|
('GetUINT32',
|
|
com.STDMETHOD(com.REFIID, POINTER(c_uint32))),
|
|
('GetUINT64',
|
|
com.STDMETHOD(com.REFIID, POINTER(c_uint64))),
|
|
('GetDouble',
|
|
com.STDMETHOD()),
|
|
('GetGUID',
|
|
com.STDMETHOD(com.REFIID, POINTER(com.GUID))),
|
|
('GetStringLength',
|
|
com.STDMETHOD()),
|
|
('GetString',
|
|
com.STDMETHOD()),
|
|
('GetAllocatedString',
|
|
com.STDMETHOD()),
|
|
('GetBlobSize',
|
|
com.STDMETHOD()),
|
|
('GetBlob',
|
|
com.STDMETHOD()),
|
|
('GetAllocatedBlob',
|
|
com.STDMETHOD()),
|
|
('GetUnknown',
|
|
com.STDMETHOD()),
|
|
('SetItem',
|
|
com.STDMETHOD()),
|
|
('DeleteItem',
|
|
com.STDMETHOD()),
|
|
('DeleteAllItems',
|
|
com.STDMETHOD()),
|
|
('SetUINT32',
|
|
com.STDMETHOD(com.REFIID, c_uint32)),
|
|
('SetUINT64',
|
|
com.STDMETHOD()),
|
|
('SetDouble',
|
|
com.STDMETHOD()),
|
|
('SetGUID',
|
|
com.STDMETHOD(com.REFIID, com.REFIID)),
|
|
('SetString',
|
|
com.STDMETHOD()),
|
|
('SetBlob',
|
|
com.STDMETHOD()),
|
|
('SetUnknown',
|
|
com.STDMETHOD(com.REFIID, com.pIUnknown)),
|
|
('LockStore',
|
|
com.STDMETHOD()),
|
|
('UnlockStore',
|
|
com.STDMETHOD()),
|
|
('GetCount',
|
|
com.STDMETHOD()),
|
|
('GetItemByIndex',
|
|
com.STDMETHOD()),
|
|
('CopyAllItems',
|
|
com.STDMETHOD(c_void_p)), # IMFAttributes
|
|
]
|
|
|
|
|
|
class IMFMediaBuffer(com.pIUnknown):
|
|
_methods_ = [
|
|
('Lock',
|
|
com.STDMETHOD(POINTER(POINTER(BYTE)), POINTER(DWORD), POINTER(DWORD))),
|
|
('Unlock',
|
|
com.STDMETHOD()),
|
|
('GetCurrentLength',
|
|
com.STDMETHOD(POINTER(DWORD))),
|
|
('SetCurrentLength',
|
|
com.STDMETHOD(DWORD)),
|
|
('GetMaxLength',
|
|
com.STDMETHOD(POINTER(DWORD)))
|
|
]
|
|
|
|
|
|
class IMFSample(IMFAttributes, com.pIUnknown):
|
|
_methods_ = [
|
|
('GetSampleFlags',
|
|
com.STDMETHOD()),
|
|
('SetSampleFlags',
|
|
com.STDMETHOD()),
|
|
('GetSampleTime',
|
|
com.STDMETHOD()),
|
|
('SetSampleTime',
|
|
com.STDMETHOD()),
|
|
('GetSampleDuration',
|
|
com.STDMETHOD(POINTER(c_ulonglong))),
|
|
('SetSampleDuration',
|
|
com.STDMETHOD(DWORD, IMFMediaBuffer)),
|
|
('GetBufferCount',
|
|
com.STDMETHOD(POINTER(DWORD))),
|
|
('GetBufferByIndex',
|
|
com.STDMETHOD(DWORD, IMFMediaBuffer)),
|
|
('ConvertToContiguousBuffer',
|
|
com.STDMETHOD(POINTER(IMFMediaBuffer))), # out
|
|
('AddBuffer',
|
|
com.STDMETHOD(POINTER(DWORD))),
|
|
('RemoveBufferByIndex',
|
|
com.STDMETHOD()),
|
|
('RemoveAllBuffers',
|
|
com.STDMETHOD()),
|
|
('GetTotalLength',
|
|
com.STDMETHOD(POINTER(DWORD))),
|
|
('CopyToBuffer',
|
|
com.STDMETHOD()),
|
|
]
|
|
|
|
|
|
class IMFMediaType(IMFAttributes, com.pIUnknown):
|
|
_methods_ = [
|
|
('GetMajorType',
|
|
com.STDMETHOD()),
|
|
('IsCompressedFormat',
|
|
com.STDMETHOD()),
|
|
('IsEqual',
|
|
com.STDMETHOD()),
|
|
('GetRepresentation',
|
|
com.STDMETHOD()),
|
|
('FreeRepresentation',
|
|
com.STDMETHOD()),
|
|
]
|
|
|
|
|
|
class IMFByteStream(com.pIUnknown):
|
|
_methods_ = [
|
|
('GetCapabilities',
|
|
com.STDMETHOD()),
|
|
('GetLength',
|
|
com.STDMETHOD()),
|
|
('SetLength',
|
|
com.STDMETHOD()),
|
|
('GetCurrentPosition',
|
|
com.STDMETHOD()),
|
|
('SetCurrentPosition',
|
|
com.STDMETHOD(c_ulonglong)),
|
|
('IsEndOfStream',
|
|
com.STDMETHOD()),
|
|
('Read',
|
|
com.STDMETHOD()),
|
|
('BeginRead',
|
|
com.STDMETHOD()),
|
|
('EndRead',
|
|
com.STDMETHOD()),
|
|
('Write',
|
|
com.STDMETHOD(POINTER(BYTE), ULONG, POINTER(ULONG))),
|
|
('BeginWrite',
|
|
com.STDMETHOD()),
|
|
('EndWrite',
|
|
com.STDMETHOD()),
|
|
('Seek',
|
|
com.STDMETHOD()),
|
|
('Flush',
|
|
com.STDMETHOD()),
|
|
('Close',
|
|
com.STDMETHOD()),
|
|
]
|
|
|
|
|
|
class IMFSourceReader(com.pIUnknown):
|
|
_methods_ = [
|
|
('GetStreamSelection',
|
|
com.STDMETHOD(DWORD, POINTER(BOOL))), # in, out
|
|
('SetStreamSelection',
|
|
com.STDMETHOD(DWORD, BOOL)),
|
|
('GetNativeMediaType',
|
|
com.STDMETHOD(DWORD, DWORD, POINTER(IMFMediaType))),
|
|
('GetCurrentMediaType',
|
|
com.STDMETHOD(DWORD, POINTER(IMFMediaType))),
|
|
('SetCurrentMediaType',
|
|
com.STDMETHOD(DWORD, POINTER(DWORD), IMFMediaType)),
|
|
('SetCurrentPosition',
|
|
com.STDMETHOD(com.REFIID, POINTER(PROPVARIANT))),
|
|
('ReadSample',
|
|
com.STDMETHOD(DWORD, DWORD, POINTER(DWORD), POINTER(DWORD), POINTER(c_longlong), POINTER(IMFSample))),
|
|
('Flush',
|
|
com.STDMETHOD(DWORD)), # in
|
|
('GetServiceForStream',
|
|
com.STDMETHOD()),
|
|
('GetPresentationAttribute',
|
|
com.STDMETHOD(DWORD, com.REFIID, POINTER(PROPVARIANT))),
|
|
]
|
|
|
|
|
|
class WAVEFORMATEX(ctypes.Structure):
|
|
_fields_ = [
|
|
('wFormatTag', WORD),
|
|
('nChannels', WORD),
|
|
('nSamplesPerSec', DWORD),
|
|
('nAvgBytesPerSec', DWORD),
|
|
('nBlockAlign', WORD),
|
|
('wBitsPerSample', WORD),
|
|
('cbSize', WORD),
|
|
]
|
|
|
|
def __repr__(self):
|
|
return 'WAVEFORMATEX(wFormatTag={}, nChannels={}, nSamplesPerSec={}, nAvgBytesPersec={}' \
|
|
', nBlockAlign={}, wBitsPerSample={}, cbSize={})'.format(
|
|
self.wFormatTag, self.nChannels, self.nSamplesPerSec,
|
|
self.nAvgBytesPerSec, self.nBlockAlign, self.wBitsPerSample,
|
|
self.cbSize)
|
|
|
|
|
|
# Stream constants
|
|
MF_SOURCE_READER_ALL_STREAMS = 0xfffffffe
|
|
MF_SOURCE_READER_ANY_STREAM = 4294967294 # 0xfffffffe
|
|
MF_SOURCE_READER_FIRST_AUDIO_STREAM = 4294967293 # 0xfffffffd
|
|
MF_SOURCE_READER_FIRST_VIDEO_STREAM = 0xfffffffc
|
|
MF_SOURCE_READER_MEDIASOURCE = 0xffffffff
|
|
|
|
# Version calculation
|
|
if WINDOWS_7_OR_GREATER:
|
|
MF_SDK_VERSION = 0x0002
|
|
else:
|
|
MF_SDK_VERSION = 0x0001
|
|
|
|
MF_API_VERSION = 0x0070 # Only used in Vista.
|
|
|
|
MF_VERSION = (MF_SDK_VERSION << 16 | MF_API_VERSION)
|
|
|
|
MFStartup = mfplat_lib.MFStartup
|
|
MFStartup.restype = HRESULT
|
|
MFStartup.argtypes = [LONG, DWORD]
|
|
|
|
MFShutdown = mfplat_lib.MFShutdown
|
|
MFShutdown.restype = HRESULT
|
|
MFShutdown.argtypes = []
|
|
|
|
MFCreateAttributes = mfplat_lib.MFCreateAttributes
|
|
MFCreateAttributes.restype = HRESULT
|
|
MFCreateAttributes.argtypes = [POINTER(IMFAttributes), c_uint32] # attributes, cInitialSize
|
|
|
|
MFCreateSourceReaderFromURL = mfreadwrite_lib.MFCreateSourceReaderFromURL
|
|
MFCreateSourceReaderFromURL.restype = HRESULT
|
|
MFCreateSourceReaderFromURL.argtypes = [LPCWSTR, IMFAttributes, POINTER(IMFSourceReader)]
|
|
|
|
MFCreateSourceReaderFromByteStream = mfreadwrite_lib.MFCreateSourceReaderFromByteStream
|
|
MFCreateSourceReaderFromByteStream.restype = HRESULT
|
|
MFCreateSourceReaderFromByteStream.argtypes = [IMFByteStream, IMFAttributes, POINTER(IMFSourceReader)]
|
|
|
|
if WINDOWS_7_OR_GREATER:
|
|
MFCreateMFByteStreamOnStream = mfplat_lib.MFCreateMFByteStreamOnStream
|
|
MFCreateMFByteStreamOnStream.restype = HRESULT
|
|
MFCreateMFByteStreamOnStream.argtypes = [c_void_p, POINTER(IMFByteStream)]
|
|
|
|
MFCreateTempFile = mfplat_lib.MFCreateTempFile
|
|
MFCreateTempFile.restype = HRESULT
|
|
MFCreateTempFile.argtypes = [UINT, UINT, UINT, POINTER(IMFByteStream)]
|
|
|
|
MFCreateMediaType = mfplat_lib.MFCreateMediaType
|
|
MFCreateMediaType.restype = HRESULT
|
|
MFCreateMediaType.argtypes = [POINTER(IMFMediaType)]
|
|
|
|
MFCreateWaveFormatExFromMFMediaType = mfplat_lib.MFCreateWaveFormatExFromMFMediaType
|
|
MFCreateWaveFormatExFromMFMediaType.restype = HRESULT
|
|
MFCreateWaveFormatExFromMFMediaType.argtypes = [IMFMediaType, POINTER(POINTER(WAVEFORMATEX)), POINTER(c_uint32), c_uint32]
|
|
|
|
|
|
class WMFSource(Source):
|
|
low_latency = True # Quicker latency but possible quality loss.
|
|
|
|
decode_audio = True
|
|
decode_video = True
|
|
|
|
def __init__(self, filename, file=None):
|
|
assert any([self.decode_audio, self.decode_video]), "Source must decode audio, video, or both, not none."
|
|
self._current_video_sample = None
|
|
self._current_video_buffer = None
|
|
self._timestamp = 0
|
|
self._attributes = None
|
|
self._stream_obj = None
|
|
self._imf_bytestream = None
|
|
self._wfx = None
|
|
self._stride = None
|
|
|
|
self.set_config_attributes()
|
|
|
|
# Create SourceReader
|
|
self._source_reader = IMFSourceReader()
|
|
|
|
# If it's a file, we need to load it as a stream.
|
|
if file is not None:
|
|
data = file.read()
|
|
|
|
self._imf_bytestream = IMFByteStream()
|
|
|
|
data_len = len(data)
|
|
|
|
if WINDOWS_7_OR_GREATER:
|
|
# Stole code from GDIPlus for older IStream support.
|
|
hglob = kernel32.GlobalAlloc(GMEM_MOVEABLE, data_len)
|
|
ptr = kernel32.GlobalLock(hglob)
|
|
ctypes.memmove(ptr, data, data_len)
|
|
kernel32.GlobalUnlock(hglob)
|
|
|
|
# Create IStream
|
|
self._stream_obj = com.pIUnknown()
|
|
ole32.CreateStreamOnHGlobal(hglob, True, ctypes.byref(self._stream_obj))
|
|
|
|
# MFCreateMFByteStreamOnStreamEx for future async operations exists, however Windows 8+ only. Requires new interface
|
|
# (Also unsure how/if new Windows async functions and callbacks work with ctypes.)
|
|
MFCreateMFByteStreamOnStream(self._stream_obj, ctypes.byref(self._imf_bytestream))
|
|
else:
|
|
# Vista does not support MFCreateMFByteStreamOnStream.
|
|
# HACK: Create file in Windows temp folder to write our byte data to.
|
|
# (Will be automatically deleted when IMFByteStream is Released.)
|
|
MFCreateTempFile(MF_ACCESSMODE_READWRITE,
|
|
MF_OPENMODE_DELETE_IF_EXIST,
|
|
MF_FILEFLAGS_NONE,
|
|
ctypes.byref(self._imf_bytestream))
|
|
|
|
wrote_length = ULONG()
|
|
data_ptr = cast(data, POINTER(BYTE))
|
|
self._imf_bytestream.Write(data_ptr, data_len, ctypes.byref(wrote_length))
|
|
self._imf_bytestream.SetCurrentPosition(0)
|
|
|
|
if wrote_length.value != data_len:
|
|
raise DecodeException("Could not write all of the data to the bytestream file.")
|
|
|
|
try:
|
|
MFCreateSourceReaderFromByteStream(self._imf_bytestream, self._attributes, ctypes.byref(self._source_reader))
|
|
except OSError as err:
|
|
raise DecodeException(err) from None
|
|
else:
|
|
# We can just load from filename if no file object specified..
|
|
try:
|
|
MFCreateSourceReaderFromURL(filename, self._attributes, ctypes.byref(self._source_reader))
|
|
except OSError as err:
|
|
raise DecodeException(err) from None
|
|
|
|
if self.decode_audio:
|
|
self._load_audio()
|
|
|
|
if self.decode_video:
|
|
self._load_video()
|
|
|
|
assert self.audio_format or self.video_format, "Source was decoded, but no video or audio streams were found."
|
|
|
|
# Get duration of the media file after everything has been ok to decode.
|
|
try:
|
|
prop = PROPVARIANT()
|
|
self._source_reader.GetPresentationAttribute(MF_SOURCE_READER_MEDIASOURCE,
|
|
ctypes.byref(MF_PD_DURATION),
|
|
ctypes.byref(prop))
|
|
|
|
self._duration = timestamp_from_wmf(prop.llVal)
|
|
ole32.PropVariantClear(ctypes.byref(prop))
|
|
except OSError:
|
|
warnings.warn("Could not determine duration of media file: '{}'.".format(filename))
|
|
|
|
def _load_audio(self, stream=MF_SOURCE_READER_FIRST_AUDIO_STREAM):
|
|
""" Prepares the audio stream for playback by detecting if it's compressed and attempting to decompress to PCM.
|
|
Default: Only get the first available audio stream.
|
|
"""
|
|
# Will be an audio file.
|
|
self._audio_stream_index = stream
|
|
|
|
# Get what the native/real media type is (audio only)
|
|
imfmedia = IMFMediaType()
|
|
|
|
try:
|
|
self._source_reader.GetNativeMediaType(self._audio_stream_index, 0, ctypes.byref(imfmedia))
|
|
except OSError as err:
|
|
if err.winerror == MF_E_INVALIDSTREAMNUMBER:
|
|
assert _debug('WMFAudioDecoder: No audio stream found.')
|
|
return
|
|
|
|
# Get Major media type (Audio, Video, etc)
|
|
# TODO: Make GUID take no arguments for a null version:
|
|
guid_audio_type = com.GUID(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
|
|
|
|
imfmedia.GetGUID(MF_MT_MAJOR_TYPE, ctypes.byref(guid_audio_type))
|
|
|
|
if guid_audio_type == MFMediaType_Audio:
|
|
assert _debug('WMFAudioDecoder: Found Audio Stream.')
|
|
|
|
# Deselect any other streams if we don't need them. (Small speedup)
|
|
if not self.decode_video:
|
|
self._source_reader.SetStreamSelection(MF_SOURCE_READER_ANY_STREAM, False)
|
|
|
|
# Select first audio stream.
|
|
self._source_reader.SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, True)
|
|
|
|
# Check sub media type, AKA what kind of codec
|
|
guid_compressed = com.GUID(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
|
|
imfmedia.GetGUID(MF_MT_SUBTYPE, ctypes.byref(guid_compressed))
|
|
|
|
if guid_compressed == MFAudioFormat_PCM or guid_compressed == MFAudioFormat_Float:
|
|
assert _debug('WMFAudioDecoder: Found Uncompressed Audio:', guid_compressed)
|
|
else:
|
|
assert _debug('WMFAudioDecoder: Found Compressed Audio:', guid_compressed)
|
|
# If audio is compressed, attempt to decompress it by forcing source reader to use PCM
|
|
mf_mediatype = IMFMediaType()
|
|
|
|
MFCreateMediaType(ctypes.byref(mf_mediatype))
|
|
mf_mediatype.SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio)
|
|
mf_mediatype.SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM)
|
|
|
|
try:
|
|
self._source_reader.SetCurrentMediaType(self._audio_stream_index, None, mf_mediatype)
|
|
except OSError as err: # Can't decode codec.
|
|
raise DecodeException(err) from None
|
|
|
|
# Current media type should now be properly decoded at this point.
|
|
decoded_media_type = IMFMediaType() # Maybe reusing older IMFMediaType will work?
|
|
self._source_reader.GetCurrentMediaType(self._audio_stream_index, ctypes.byref(decoded_media_type))
|
|
|
|
wfx_length = ctypes.c_uint32()
|
|
wfx = POINTER(WAVEFORMATEX)()
|
|
|
|
MFCreateWaveFormatExFromMFMediaType(decoded_media_type,
|
|
ctypes.byref(wfx),
|
|
ctypes.byref(wfx_length),
|
|
0)
|
|
|
|
self._wfx = wfx.contents
|
|
self.audio_format = AudioFormat(channels=self._wfx.nChannels,
|
|
sample_size=self._wfx.wBitsPerSample,
|
|
sample_rate=self._wfx.nSamplesPerSec)
|
|
else:
|
|
assert _debug('WMFAudioDecoder: Audio stream not found')
|
|
|
|
def get_format(self):
|
|
"""Returns the WAVEFORMATEX data which has more information thah audio_format"""
|
|
return self._wfx
|
|
|
|
def _load_video(self, stream=MF_SOURCE_READER_FIRST_VIDEO_STREAM):
|
|
self._video_stream_index = stream
|
|
|
|
# Get what the native/real media type is (video only)
|
|
imfmedia = IMFMediaType()
|
|
|
|
try:
|
|
self._source_reader.GetCurrentMediaType(self._video_stream_index, ctypes.byref(imfmedia))
|
|
except OSError as err:
|
|
if err.winerror == MF_E_INVALIDSTREAMNUMBER:
|
|
assert _debug('WMFVideoDecoder: No video stream found.')
|
|
return
|
|
|
|
assert _debug('WMFVideoDecoder: Found Video Stream')
|
|
|
|
# All video is basically compressed, try to decompress.
|
|
uncompressed_mt = IMFMediaType()
|
|
MFCreateMediaType(ctypes.byref(uncompressed_mt))
|
|
|
|
imfmedia.CopyAllItems(uncompressed_mt)
|
|
|
|
imfmedia.Release()
|
|
|
|
uncompressed_mt.SetGUID(MF_MT_SUBTYPE, MFVideoFormat_ARGB32)
|
|
uncompressed_mt.SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive)
|
|
uncompressed_mt.SetUINT32(MF_MT_ALL_SAMPLES_INDEPENDENT, 1)
|
|
|
|
try:
|
|
self._source_reader.SetCurrentMediaType(self._video_stream_index, None, uncompressed_mt)
|
|
except OSError as err: # Can't decode codec.
|
|
raise DecodeException(err) from None
|
|
|
|
height, width = self._get_attribute_size(uncompressed_mt, MF_MT_FRAME_SIZE)
|
|
|
|
self.video_format = VideoFormat(width=width, height=height)
|
|
assert _debug('WMFVideoDecoder: Frame width: {} height: {}'.format(width, height))
|
|
|
|
# Frame rate
|
|
den, num = self._get_attribute_size(uncompressed_mt, MF_MT_FRAME_RATE)
|
|
self.video_format.frame_rate = num / den
|
|
assert _debug('WMFVideoDecoder: Frame Rate: {} / {} = {}'.format(num, den, self.video_format.frame_rate))
|
|
|
|
# Sometimes it can return negative? Variable bit rate? Needs further tests and examples.
|
|
if self.video_format.frame_rate < 0:
|
|
self.video_format.frame_rate = 30000 / 1001
|
|
assert _debug('WARNING: Negative frame rate, attempting to use default, but may experience issues.')
|
|
|
|
# Pixel ratio
|
|
den, num = self._get_attribute_size(uncompressed_mt, MF_MT_PIXEL_ASPECT_RATIO)
|
|
self.video_format.sample_aspect = num / den
|
|
assert _debug('WMFVideoDecoder: Pixel Ratio: {} / {} = {}'.format(num, den, self.video_format.sample_aspect))
|
|
|
|
def get_audio_data(self, num_bytes, compensation_time=0.0):
|
|
flags = DWORD()
|
|
timestamp = ctypes.c_longlong()
|
|
|
|
imf_sample = IMFSample()
|
|
imf_buffer = IMFMediaBuffer()
|
|
|
|
while True:
|
|
self._source_reader.ReadSample(self._audio_stream_index, 0, None, ctypes.byref(flags),
|
|
ctypes.byref(timestamp), ctypes.byref(imf_sample))
|
|
|
|
if flags.value & MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED:
|
|
assert _debug('WMFAudioDecoder: Data is no longer valid.')
|
|
break
|
|
|
|
if flags.value & MF_SOURCE_READERF_ENDOFSTREAM:
|
|
assert _debug('WMFAudioDecoder: End of data from stream source.')
|
|
break
|
|
|
|
if not imf_sample:
|
|
assert _debug('WMFAudioDecoder: No sample.')
|
|
continue
|
|
|
|
# Convert to single buffer as a sample could potentially(rarely) have multiple buffers.
|
|
imf_sample.ConvertToContiguousBuffer(ctypes.byref(imf_buffer))
|
|
|
|
audio_data_ptr = POINTER(BYTE)()
|
|
audio_data_length = DWORD()
|
|
|
|
imf_buffer.Lock(ctypes.byref(audio_data_ptr), None, ctypes.byref(audio_data_length))
|
|
|
|
audio_data = ctypes.string_at(audio_data_ptr, audio_data_length.value)
|
|
|
|
imf_buffer.Unlock()
|
|
imf_buffer.Release()
|
|
imf_sample.Release()
|
|
|
|
return AudioData(audio_data,
|
|
audio_data_length.value,
|
|
timestamp_from_wmf(timestamp.value),
|
|
audio_data_length.value / self.audio_format.sample_rate,
|
|
[])
|
|
|
|
return None
|
|
|
|
def get_next_video_frame(self, skip_empty_frame=True):
|
|
video_data_length = DWORD()
|
|
flags = DWORD()
|
|
timestamp = ctypes.c_longlong()
|
|
|
|
if self._current_video_sample:
|
|
self._current_video_buffer.Release()
|
|
self._current_video_sample.Release()
|
|
|
|
self._current_video_sample = IMFSample()
|
|
self._current_video_buffer = IMFMediaBuffer()
|
|
|
|
while True:
|
|
self._source_reader.ReadSample(self._video_stream_index, 0, None, ctypes.byref(flags),
|
|
ctypes.byref(timestamp), ctypes.byref(self._current_video_sample))
|
|
|
|
if flags.value & MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED:
|
|
assert _debug('WMFVideoDecoder: Data is no longer valid.')
|
|
|
|
# Get Major media type (Audio, Video, etc)
|
|
new = IMFMediaType()
|
|
self._source_reader.GetCurrentMediaType(self._video_stream_index, ctypes.byref(new))
|
|
|
|
# Sometimes this happens once. I think this only
|
|
# changes if the stride is added/changed before playback?
|
|
stride = ctypes.c_uint32()
|
|
new.GetUINT32(MF_MT_DEFAULT_STRIDE, ctypes.byref(stride))
|
|
new.Release()
|
|
|
|
self._stride = stride.value
|
|
|
|
if flags.value & MF_SOURCE_READERF_ENDOFSTREAM:
|
|
self._timestamp = None
|
|
assert _debug('WMFVideoDecoder: End of data from stream source.')
|
|
break
|
|
|
|
if not self._current_video_sample:
|
|
assert _debug('WMFVideoDecoder: No sample.')
|
|
continue
|
|
|
|
self._current_video_buffer = IMFMediaBuffer()
|
|
|
|
# Convert to single buffer as a sample could potentially have multiple buffers.
|
|
self._current_video_sample.ConvertToContiguousBuffer(ctypes.byref(self._current_video_buffer))
|
|
|
|
video_data = POINTER(BYTE)()
|
|
|
|
self._current_video_buffer.Lock(ctypes.byref(video_data), None, ctypes.byref(video_data_length))
|
|
|
|
width = self.video_format.width
|
|
height = self.video_format.height
|
|
|
|
# buffer = ctypes.create_string_buffer(size)
|
|
self._timestamp = timestamp_from_wmf(timestamp.value)
|
|
|
|
self._current_video_buffer.Unlock()
|
|
|
|
# This is made with the assumption that the video frame will be blitted into the player texture immediately
|
|
# after, and then cleared next frame attempt.
|
|
return image.ImageData(width, height, 'BGRA', video_data, self._stride)
|
|
|
|
return None
|
|
|
|
def get_next_video_timestamp(self):
|
|
return self._timestamp
|
|
|
|
def seek(self, timestamp):
|
|
timestamp = min(timestamp, self._duration) if self._duration else timestamp
|
|
|
|
prop = PROPVARIANT()
|
|
prop.vt = VT_I8
|
|
prop.llVal = timestamp_to_wmf(timestamp)
|
|
|
|
pos_com = com.GUID(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
|
|
try:
|
|
self._source_reader.SetCurrentPosition(pos_com, prop)
|
|
except OSError as err:
|
|
warnings.warn(str(err))
|
|
|
|
ole32.PropVariantClear(ctypes.byref(prop))
|
|
|
|
@staticmethod
|
|
def _get_attribute_size(attributes, guidKey):
|
|
""" Convert int64 attributes to int32""" # HI32/LOW32
|
|
|
|
size = ctypes.c_uint64()
|
|
attributes.GetUINT64(guidKey, size)
|
|
lParam = size.value
|
|
|
|
x = ctypes.c_int32(lParam).value
|
|
y = ctypes.c_int32(lParam >> 32).value
|
|
return x, y
|
|
|
|
def set_config_attributes(self):
|
|
""" Here we set user specified attributes, by default we try to set low latency mode. (Win7+)"""
|
|
if self.low_latency or self.decode_video:
|
|
self._attributes = IMFAttributes()
|
|
|
|
MFCreateAttributes(ctypes.byref(self._attributes), 3)
|
|
|
|
if self.low_latency and WINDOWS_7_OR_GREATER:
|
|
self._attributes.SetUINT32(ctypes.byref(MF_LOW_LATENCY), 1)
|
|
|
|
assert _debug('WMFAudioDecoder: Setting configuration attributes.')
|
|
|
|
# If it's a video we need to enable the streams to be accessed.
|
|
if self.decode_video:
|
|
self._attributes.SetUINT32(ctypes.byref(MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS), 1)
|
|
self._attributes.SetUINT32(ctypes.byref(MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING), 1)
|
|
|
|
assert _debug('WMFVideoDecoder: Setting configuration attributes.')
|
|
|
|
def __del__(self):
|
|
if self._source_reader:
|
|
self._source_reader.Release()
|
|
|
|
if self._stream_obj:
|
|
self._stream_obj.Release()
|
|
|
|
if self._imf_bytestream:
|
|
self._imf_bytestream.Release()
|
|
|
|
if self._current_video_sample:
|
|
self._current_video_buffer.Release()
|
|
self._current_video_sample.Release()
|
|
|
|
|
|
#########################################
|
|
# Decoder class:
|
|
#########################################
|
|
|
|
class WMFDecoder(MediaDecoder):
|
|
def __init__(self):
|
|
self.MFShutdown = None
|
|
|
|
try:
|
|
MFStartup(MF_VERSION, 0)
|
|
except OSError as err:
|
|
raise ImportError('WMF could not startup:', err.strerror)
|
|
|
|
self.extensions = self._build_decoder_extensions()
|
|
|
|
self.MFShutdown = MFShutdown
|
|
|
|
assert _debug('Windows Media Foundation: Initialized.')
|
|
|
|
@staticmethod
|
|
def _build_decoder_extensions():
|
|
"""Extension support varies depending on OS version."""
|
|
extensions = []
|
|
if WINDOWS_VISTA_OR_GREATER:
|
|
extensions.extend(['.asf', '.wma', '.wmv',
|
|
'.mp3',
|
|
'.sami', '.smi',
|
|
])
|
|
|
|
if WINDOWS_7_OR_GREATER:
|
|
extensions.extend(['.3g2', '.3gp', '.3gp2', '.3gp',
|
|
'.aac', '.adts',
|
|
'.avi',
|
|
'.m4a', '.m4v',
|
|
# '.wav' # Can do wav, but we have a WAVE decoder.
|
|
])
|
|
|
|
if WINDOWS_10_ANNIVERSARY_UPDATE_OR_GREATER:
|
|
extensions.extend(['.flac'])
|
|
|
|
return extensions
|
|
|
|
def get_file_extensions(self):
|
|
return self.extensions
|
|
|
|
def decode(self, filename, file, streaming=True):
|
|
if streaming:
|
|
return WMFSource(filename, file)
|
|
else:
|
|
return StaticSource(WMFSource(filename, file))
|
|
|
|
def __del__(self):
|
|
if self.MFShutdown is not None:
|
|
self.MFShutdown()
|
|
|
|
|
|
def get_decoders():
|
|
return [WMFDecoder()]
|
|
|
|
|
|
def get_encoders():
|
|
return []
|