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 []
|