# ---------------------------------------------------------------------------- # pyglet # Copyright (c) 2006-2008 Alex Holkner # Copyright (c) 2008-2021 pyglet contributors # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in # the documentation and/or other materials provided with the # distribution. # * Neither the name of pyglet nor the names of its # contributors may be used to endorse or promote products # derived from this software without specific prior written # permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # ---------------------------------------------------------------------------- import os import platform import warnings from pyglet import com, image from pyglet.util import debug_print from pyglet.libs.win32 import _kernel32 as kernel32 from pyglet.libs.win32 import _ole32 as ole32 from pyglet.libs.win32.constants import * from pyglet.libs.win32.types import * from pyglet.media import Source, MediaDecodeException from pyglet.media.codecs import AudioFormat, AudioData, VideoFormat, MediaDecoder, StaticSource _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_audio_sample = None self._current_audio_buffer = 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 MediaDecodeException("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 MediaDecodeException(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 MediaDecodeException(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 MediaDecodeException(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 MediaDecodeException(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() audio_data_length = DWORD() # If we have an audio sample already in use and we call this again, release the memory of buffer and sample. # Can only release after the data is played or else glitches and pops can be heard. if self._current_audio_sample: self._current_audio_buffer.Release() self._current_audio_sample.Release() self._current_audio_sample = IMFSample() self._current_audio_buffer = IMFMediaBuffer() while True: self._source_reader.ReadSample(self._audio_stream_index, 0, None, ctypes.byref(flags), ctypes.byref(timestamp), ctypes.byref(self._current_audio_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 self._current_audio_sample: assert _debug('WMFAudioDecoder: No sample.') continue # Convert to single buffer as a sample could potentially(rarely) have multiple buffers. self._current_audio_sample.ConvertToContiguousBuffer(ctypes.byref(self._current_audio_buffer)) audio_data_ptr = POINTER(BYTE)() self._current_audio_buffer.Lock(ctypes.byref(audio_data_ptr), None, ctypes.byref(audio_data_length)) self._current_audio_buffer.Unlock() audio_data = create_string_buffer(audio_data_length.value) memmove(audio_data, audio_data_ptr, audio_data_length.value) 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)) 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._stream_obj: self._stream_obj.Release() if self._imf_bytestream: self._imf_bytestream.Release() if self._current_audio_sample: self._current_audio_buffer.Release() self._current_audio_sample.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.ole32 = None self.MFShutdown = None try: # Coinitialize supposed to be called for COMs? ole32.CoInitializeEx(None, COINIT_MULTITHREADED) except OSError as err: warnings.warn(str(err)) try: MFStartup(MF_VERSION, 0) except OSError as err: raise ImportError('WMF could not startup:', err.strerror) self.extensions = self._build_decoder_extensions() self.ole32 = ole32 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', '.mov', '.mp4', # '.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, file, filename, 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() if self.ole32 is not None: self.ole32.CoUninitialize() def get_decoders(): return [WMFDecoder()] def get_encoders(): return []