sync pyget

This commit is contained in:
shenjack 2023-06-27 01:05:51 +08:00
parent 8d3e14fc39
commit 1e342ce0a8
11 changed files with 274 additions and 199 deletions

View File

@ -122,12 +122,24 @@ class EventLoop(event.EventDispatcher):
def run(self, interval=1/60):
"""Begin processing events, scheduled functions and window updates.
:Parameters:
`interval` : float or None [default: 1/60]
Windows redraw interval, in seconds (framerate).
If `interval == 0`, windows will redraw at maximum rate.
If `interval is None`, Pyglet will not call its redraw function.
The user must schedule (or call on demand) a custom redraw
function for each window, allowing a custom framerate per window.
(see example in documentation)
This method returns when :py:attr:`has_exit` is set to True.
Developers are discouraged from overriding this method, as the
implementation is platform-specific.
"""
if not interval:
if interval is None:
# User application will manage a custom _redraw_windows() method
pass
elif interval == 0:
self.clock.schedule(self._redraw_windows)
else:
self.clock.schedule_interval(self._redraw_windows, interval)

View File

@ -135,6 +135,17 @@ class AudioData:
self.duration -= num_bytes / audio_format.bytes_per_second
self.timestamp += num_bytes / audio_format.bytes_per_second
def get_string_data(self):
"""Return data as a bytestring.
Returns:
bytes: Data as a (byte)string.
"""
if self.data is None:
return b''
return memoryview(self.data).tobytes()[:self.length]
class SourceInfo:
"""Source metadata information.
@ -398,7 +409,7 @@ class StaticSource(Source):
audio_data = source.get_audio_data(buffer_size)
if not audio_data:
break
data.write(audio_data.data)
data.write(audio_data.get_string_data())
self._data = data.getvalue()
self._duration = len(self._data) / self.audio_format.bytes_per_second

View File

@ -1,38 +1,40 @@
from abc import ABCMeta, abstractmethod
from enum import Enum, auto
from typing import Dict, Optional
from pyglet import event
from pyglet.util import with_metaclass
class DeviceState:
ACTIVE = "active"
DISABLED = "disabled"
MISSING = "missing"
UNPLUGGED = "unplugged"
class DeviceState(Enum):
ACTIVE = auto()
DISABLED = auto()
MISSING = auto()
UNPLUGGED = auto()
class DeviceFlow:
OUTPUT = "output"
INPUT = "input"
INPUT_OUTPUT = "input/output"
class DeviceFlow(Enum):
OUTPUT = auto()
INPUT = auto()
INPUT_OUTPUT = auto()
class AudioDevice:
"""Base class for a platform independent audio device.
_platform_state and _platform_flow is used to make device state numbers."""
_platform_state = {} # Must be defined by the parent.
_platform_flow = {} # Must be defined by the parent.
platform_state: Dict[int, DeviceState] = {} # Must be defined by the parent.
platform_flow: Dict[int, DeviceFlow] = {} # Must be defined by the parent.
def __init__(self, dev_id, name, description, flow, state):
def __init__(self, dev_id: str, name: str, description: str, flow: int, state: int):
self.id = dev_id
self.flow = flow
self.state = state
self.flow = flow # platform value
self.state = state # platform value
self.name = name
self.description = description
def __repr__(self):
return "{}(name={}, state={}, flow={})".format(
self.__class__.__name__, self.name, self._platform_state[self.state], self._platform_flow[self.flow])
return "{}(name='{}', state={}, flow={})".format(
self.__class__.__name__, self.name, self.platform_state[self.state].name, self.platform_flow[self.flow].name)
class AbstractAudioDeviceManager(with_metaclass(ABCMeta, event.EventDispatcher, object)):
@ -66,20 +68,23 @@ class AbstractAudioDeviceManager(with_metaclass(ABCMeta, event.EventDispatcher,
"""Returns a list of all audio devices, no matter what state they are in."""
pass
def on_device_state_changed(self, device, old_state, new_state):
"""Event, occurs when the state of a device changes, provides old state and new state."""
def on_device_state_changed(self, device: AudioDevice, old_state: DeviceState, new_state: DeviceState):
"""Event, occurs when the state of a device changes, provides the old state and new state."""
pass
def on_device_added(self, device):
def on_device_added(self, device: AudioDevice):
"""Event, occurs when a new device is added to the system."""
pass
def on_device_removed(self, device):
def on_device_removed(self, device: AudioDevice):
"""Event, occurs when an existing device is removed from the system."""
pass
def on_default_changed(self, device):
"""Event, occurs when the default audio device changes."""
def on_default_changed(self, device: Optional[AudioDevice], flow: DeviceFlow):
"""Event, occurs when the default audio device changes.
If there is no device that can be the default on the system, can be None.
The flow determines whether an input or output device became it's respective default.
"""
pass

View File

@ -1,3 +1,5 @@
from typing import List, Optional, Tuple
from pyglet.libs.win32 import com
from pyglet.libs.win32 import _ole32 as ole32
from pyglet.libs.win32.constants import CLSCTX_INPROC_SERVER
@ -5,7 +7,7 @@ from pyglet.libs.win32.types import *
from pyglet.media.devices import base
from pyglet.util import debug_print
_debug = debug_print('debug_media')
_debug = debug_print('debug_input')
EDataFlow = UINT
# Audio rendering stream. Audio data flows from the application to the audio endpoint device, which renders the stream.
@ -49,7 +51,7 @@ class PROPERTYKEY(ctypes.Structure):
return "PROPERTYKEY({}, pid={})".format(self.fmtid, self.pid)
REFPROPERTYKEY = PROPERTYKEY
REFPROPERTYKEY = POINTER(PROPERTYKEY)
PKEY_Device_FriendlyName = PROPERTYKEY(
com.GUID(0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0), 14)
@ -93,10 +95,20 @@ class IMMNotificationClient(com.IUnknown):
]
IID_IMMEndpoint = com.GUID(0x1BE09788, 0x6894, 0x4089, 0x85, 0x86, 0x9A, 0x2A, 0x6C, 0x26, 0x5A, 0xC5)
class IMMEndpoint(com.pIUnknown):
_methods_ = [
('GetDataFlow',
com.STDMETHOD(POINTER(EDataFlow))),
]
class AudioNotificationCB(com.COMObject):
_interfaces_ = [IMMNotificationClient]
def __init__(self, audio_devices):
def __init__(self, audio_devices: 'Win32AudioDeviceManager'):
super().__init__()
self.audio_devices = audio_devices
self._lost = False
@ -105,19 +117,24 @@ class AudioNotificationCB(com.COMObject):
device = self.audio_devices.get_cached_device(pwstrDeviceId)
old_state = device.state
pyglet_old_state = Win32AudioDevice.platform_state[old_state]
pyglet_new_state = Win32AudioDevice.platform_state[dwNewState]
assert _debug(
"Audio device {} changed state. From state: {} to state: {}".format(device.name, old_state, dwNewState))
f"Audio device '{device.name}' changed state. From: {pyglet_old_state} to: {pyglet_new_state}")
device.state = dwNewState
self.audio_devices.dispatch_event('on_device_state_changed', device, old_state, dwNewState)
self.audio_devices.dispatch_event('on_device_state_changed', device, pyglet_old_state, pyglet_new_state)
def OnDeviceAdded(self, this, pwstrDeviceId):
assert _debug("Audio device was added {}".format(pwstrDeviceId))
self.audio_devices.dispatch_event('on_device_added', pwstrDeviceId)
dev = self.audio_devices.add_device(pwstrDeviceId)
assert _debug(f"Audio device was added {pwstrDeviceId}: {dev}")
self.audio_devices.dispatch_event('on_device_added', dev)
def OnDeviceRemoved(self, this, pwstrDeviceId):
assert _debug("Audio device was removed {}".format(pwstrDeviceId))
self.audio_devices.dispatch_event('on_device_removed', pwstrDeviceId)
dev = self.audio_devices.remove_device(pwstrDeviceId)
assert _debug(f"Audio device was removed {pwstrDeviceId} : {dev}")
self.audio_devices.dispatch_event('on_device_removed', dev)
def OnDefaultDeviceChanged(self, this, flow, role, pwstrDeviceId):
# Only support eConsole role right now
@ -127,7 +144,10 @@ class AudioNotificationCB(com.COMObject):
else:
device = self.audio_devices.get_cached_device(pwstrDeviceId)
self.audio_devices.dispatch_event('on_default_changed', device)
pyglet_flow = Win32AudioDevice.platform_flow[flow]
assert _debug(f"Default device was changed to: {device} ({pyglet_flow})")
self.audio_devices.dispatch_event('on_default_changed', device, pyglet_flow)
def OnPropertyValueChanged(self, this, pwstrDeviceId, key):
pass
@ -171,14 +191,14 @@ class IMMDeviceEnumerator(com.pIUnknown):
class Win32AudioDevice(base.AudioDevice):
_platform_state = {
platform_state = {
DEVICE_STATE_ACTIVE: base.DeviceState.ACTIVE,
DEVICE_STATE_DISABLED: base.DeviceState.DISABLED,
DEVICE_STATE_NOTPRESENT: base.DeviceState.MISSING,
DEVICE_STATE_UNPLUGGED: base.DeviceState.UNPLUGGED
}
_platform_flow = {
platform_flow = {
eRender: base.DeviceFlow.OUTPUT,
eCapture: base.DeviceFlow.INPUT,
eAll: base.DeviceFlow.INPUT_OUTPUT
@ -192,14 +212,42 @@ class Win32AudioDeviceManager(base.AbstractAudioDeviceManager):
byref(self._device_enum))
# Keep all devices cached, and the callback can keep them updated.
self.devices = self._query_all_devices()
self.devices: List[Win32AudioDevice] = self._query_all_devices()
super().__init__()
self._callback = AudioNotificationCB(self)
self._device_enum.RegisterEndpointNotificationCallback(self._callback)
def get_default_output(self):
def add_device(self, pwstrDeviceId: str) -> Win32AudioDevice:
dev = self.get_device(pwstrDeviceId)
self.devices.append(dev)
return dev
def remove_device(self, pwstrDeviceId: str) -> Win32AudioDevice:
dev = self.audio_devices.get_cached_device(pwstrDeviceId)
self.audio_devices.devices.remove(dev)
return dev
def get_device(self, pwstrDeviceId: str) -> Win32AudioDevice:
device = IMMDevice()
self._device_enum.GetDevice(pwstrDeviceId, byref(device))
dev_id, name, desc, dev_state = self.get_device_info(device)
ep = IMMEndpoint()
device.QueryInterface(IID_IMMEndpoint, byref(ep))
dataflow = EDataFlow()
ep.GetDataFlow(byref(dataflow))
flow = dataflow.value
windevice = Win32AudioDevice(dev_id, name, desc, flow, dev_state)
ep.Release()
device.Release()
return windevice
def get_default_output(self) -> Optional[Win32AudioDevice]:
"""Attempts to retrieve a default audio output for the system. Returns None if no available devices found."""
try:
device = IMMDevice()
@ -207,14 +255,14 @@ class Win32AudioDeviceManager(base.AbstractAudioDeviceManager):
dev_id, name, desc, dev_state = self.get_device_info(device)
device.Release()
pid = self.get_cached_device(dev_id)
pid.state = dev_state
return pid
except OSError:
assert _debug("No default audio output was found.")
cached_dev = self.get_cached_device(dev_id)
cached_dev.state = dev_state
return cached_dev
except OSError as err:
assert _debug("No default audio output was found.", err)
return None
def get_default_input(self):
def get_default_input(self) -> Optional[Win32AudioDevice]:
"""Attempts to retrieve a default audio input for the system. Returns None if no available devices found."""
try:
device = IMMDevice()
@ -222,37 +270,35 @@ class Win32AudioDeviceManager(base.AbstractAudioDeviceManager):
dev_id, name, desc, dev_state = self.get_device_info(device)
device.Release()
pid = self.get_cached_device(dev_id)
pid.state = dev_state
return pid
except OSError:
assert _debug("No default input output was found.")
cached_dev = self.get_cached_device(dev_id)
cached_dev.state = dev_state
return cached_dev
except OSError as err:
assert _debug("No default input output was found.", err)
return None
def get_cached_device(self, dev_id):
def get_cached_device(self, dev_id) -> Win32AudioDevice:
"""Gets the cached devices, so we can reduce calls to COM and tell current state vs new states."""
for device in self.devices:
if device.id == dev_id:
return device
raise Exception("Attempted to get a device that does not exist.")
raise Exception("Attempted to get a device that does not exist.", dev_id)
# return None
def get_output_devices(self, state=DEVICE_STATE_ACTIVE):
def get_output_devices(self, state=DEVICE_STATE_ACTIVE) -> List[Win32AudioDevice]:
return [device for device in self.devices if device.state == state and device.flow == eRender]
def get_input_devices(self, state=DEVICE_STATE_ACTIVE):
def get_input_devices(self, state=DEVICE_STATE_ACTIVE) -> List[Win32AudioDevice]:
return [device for device in self.devices if device.state == state and device.flow == eCapture]
def get_all_devices(self):
def get_all_devices(self) -> List[Win32AudioDevice]:
return self.devices
def _query_all_devices(self):
def _query_all_devices(self) -> List[Win32AudioDevice]:
return self.get_devices(flow=eRender, state=DEVICE_STATEMASK_ALL) + self.get_devices(flow=eCapture,
state=DEVICE_STATEMASK_ALL)
def get_device_info(self, device):
def get_device_info(self, device: IMMDevice) -> Tuple[str, str, str, int]:
"""Return the ID, Name, and Description of the Audio Device."""
store = IPropertyStore()
device.OpenPropertyStore(STGM_READ, byref(store))
@ -293,13 +339,13 @@ class Win32AudioDeviceManager(base.AbstractAudioDeviceManager):
return devices
@staticmethod
def get_pkey_value(store, pkey):
def get_pkey_value(store: IPropertyStore, pkey: PROPERTYKEY):
try:
propvar = PROPVARIANT()
store.GetValue(pkey, byref(propvar))
value = propvar.pwszVal
ole32.PropVariantClear(propvar)
except Exception as err:
ole32.PropVariantClear(byref(propvar))
except Exception:
value = "Unknown"
return value

View File

@ -165,7 +165,10 @@ class AbstractAudioPlayer(with_metaclass(ABCMeta)):
@property
def source(self):
"""Source to play from."""
"""Source to play from.
May be swapped out for one of an equal audio format, but ensure that
the player has been paused and cleared beforehand.
"""
return self._source
@source.setter

View File

@ -69,6 +69,10 @@ class DirectSoundAudioPlayer(AbstractAudioPlayer):
# eos for one buffer size.
self._eos_cursor = None
# Whether the source has hit its end; protect against duplicate
# dispatch of on_eos events.
self._has_underrun = False
# Indexes into DSound circular buffer. Complications ensue wrt each
# other to avoid writing over the play cursor. See _get_write_size and
# write().
@ -101,13 +105,13 @@ class DirectSoundAudioPlayer(AbstractAudioPlayer):
def play(self):
assert _debug('DirectSound play')
self.driver.worker.add(self)
if not self._playing:
self._get_audiodata() # prebuffer if needed
self._playing = True
self._get_audiodata() # prebuffer if needed
self._ds_buffer.play()
self.driver.worker.add(self)
assert _debug('return DirectSound play')
def stop(self):
@ -128,6 +132,7 @@ class DirectSoundAudioPlayer(AbstractAudioPlayer):
self._play_cursor = self._write_cursor
self._eos_cursor = None
self._audiodata_buffer = None
self._has_underrun = False
del self._events[:]
del self._timestamps[:]
@ -153,10 +158,6 @@ class DirectSoundAudioPlayer(AbstractAudioPlayer):
self.write(None, write_size)
write_size = 0
def _has_underrun(self):
return (self._eos_cursor is not None
and self._play_cursor > self._eos_cursor)
def _dispatch_new_event(self, event_name):
MediaEvent(event_name).sync_dispatch_to_player(self.player)
@ -243,9 +244,13 @@ class DirectSoundAudioPlayer(AbstractAudioPlayer):
del self._timestamps[0]
def _check_underrun(self):
if self._playing and self._has_underrun():
if (
not self._has_underrun and
self._playing and
(self._eos_cursor is not None and self._play_cursor > self._eos_cursor)
):
assert _debug('underrun, stopping')
self.stop()
self._has_underrun = True
self._dispatch_new_event('on_eos')
def _get_write_size(self):

View File

@ -101,6 +101,10 @@ class OpenALAudioPlayer(AbstractAudioPlayer):
# Cursor position of end of queued AL buffer.
self._write_cursor = 0
# Whether the source hit its end; protect against duplicate dispatch
# of on_eos events.
self._has_underrun = False
# List of currently queued buffer sizes (in bytes)
self._buffer_sizes = []
@ -177,6 +181,7 @@ class OpenALAudioPlayer(AbstractAudioPlayer):
self._buffer_cursor = 0
self._play_cursor = 0
self._write_cursor = 0
self._has_underrun = False
del self._events[:]
del self._buffer_sizes[:]
del self._buffer_timestamps[:]
@ -278,8 +283,8 @@ class OpenALAudioPlayer(AbstractAudioPlayer):
self._queue_events(self._audiodata_buffer)
else:
assert _debug('No audio data left')
if self._has_underrun():
assert _debug('Underrun')
if self._has_just_underrun():
assert _debug('Freshly underrun')
MediaEvent('on_eos').sync_dispatch_to_player(self.player)
def _queue_audio_data(self, audio_data, length):
@ -297,12 +302,17 @@ class OpenALAudioPlayer(AbstractAudioPlayer):
def _queue_events(self, audio_data):
for event in audio_data.events:
cursor = self._write_cursor + event.timestamp * \
self.source.audio_format.bytes_per_second
cursor = self._write_cursor + event.timestamp * self.source.audio_format.bytes_per_second
self._events.append((cursor, event))
def _has_underrun(self):
return self.alsource.buffers_queued == 0
def _has_just_underrun(self):
if self._has_underrun:
return False
if self.alsource.buffers_queued == 0:
self._has_underrun = True
return self._has_underrun
def get_time(self):
# Update first, might remove buffers

View File

@ -89,7 +89,6 @@ class XAudio2AudioPlayer(AbstractAudioPlayer):
if not self._buffers:
self._xa2_driver.return_voice(self._xa2_source_voice)
def play(self):
assert _debug('XAudio2 play')
@ -137,6 +136,8 @@ class XAudio2AudioPlayer(AbstractAudioPlayer):
Unlike the other drivers this does not carve pieces of audio from the buffer and slowly
consume it. This submits the buffer retrieved from the decoder in it's entirety.
"""
if not self._xa2_source_voice:
return
buffers_queued = self._xa2_source_voice.buffers_queued

View File

@ -1,6 +1,8 @@
import weakref
from collections import namedtuple, defaultdict
from pyglet.media.devices.base import DeviceFlow
import pyglet
from pyglet.libs.win32.types import *
from pyglet.util import debug_print
@ -69,7 +71,8 @@ class XAudio2Driver:
self._players.clear()
def on_default_changed(self, device):
def on_default_changed(self, device, flow: DeviceFlow):
if flow == DeviceFlow.OUTPUT:
"""Callback derived from the Audio Devices to help us determine when the system no longer has output."""
if device is None:
assert _debug('Error: Default audio device was removed or went missing.')

View File

@ -10,143 +10,120 @@ from pyglet.util import debug_print
_debug = debug_print('debug_media')
class MediaThread:
"""A thread that cleanly exits on interpreter shutdown, and provides
a sleep method that can be interrupted and a termination method.
:Ivariables:
`_condition` : threading.Condition
Lock _condition on all instance variables.
`_stopped` : bool
True if `stop` has been called.
class PlayerWorkerThread(threading.Thread):
"""Worker thread for refilling players. Exits on interpreter shutdown,
provides a notify method to interrupt it as well as a termination method.
"""
_threads = set()
_threads_lock = threading.Lock()
# Time to wait if there are players, but they're all full:
_nap_time = 0.05
def __init__(self):
self._thread = threading.Thread(target=self._thread_run, daemon=True)
self._condition = threading.Condition()
super().__init__(daemon=True)
self._rest_event = threading.Event()
# A lock that should be held as long as consistency of `self.players` is required.
self._operation_lock = threading.Lock()
self._stopped = False
self.players = set()
def run(self):
raise NotImplementedError
def _thread_run(self):
if pyglet.options['debug_trace']:
pyglet._install_trace()
with self._threads_lock:
self._threads.add(self)
self.run()
with self._threads_lock:
self._threads.remove(self)
def start(self):
self._thread.start()
sleep_time = None
while True:
assert _debug(
'PlayerWorkerThread: Going to sleep ' +
('indefinitely; no active players' if sleep_time is None else f'for {sleep_time}')
)
self._rest_event.wait(sleep_time)
self._rest_event.clear()
assert _debug(f'PlayerWorkerThread: woke up @{time.time()}')
if self._stopped:
break
with self._operation_lock:
if self.players:
sleep_time = self._nap_time
for player in self.players:
player.refill_buffer()
else:
# sleep until a player is added
sleep_time = None
self._threads.remove(self)
def stop(self):
"""Stop the thread and wait for it to terminate.
The `stop` instance variable is set to ``True`` and the condition is
notified. It is the responsibility of the `run` method to check
the value of `stop` after each sleep or wait and to return if set.
The `stop` instance variable is set to ``True`` and the rest event
is set. It is the responsibility of the `run` method to check
the value of `_stopped` after each sleep or wait and to return if
set.
"""
assert _debug('MediaThread.stop()')
with self._condition:
assert _debug('PlayerWorkerThread.stop()')
self._stopped = True
self._condition.notify()
self._rest_event.set()
try:
self._thread.join()
self.join()
except RuntimeError:
# Ignore on unclean shutdown
pass
def sleep(self, timeout):
"""Wait for some amount of time, or until notified.
:Parameters:
`timeout` : float
Time to wait, in seconds.
"""
assert _debug(f'MediaThread.sleep({timeout!r})')
with self._condition:
if not self._stopped:
self._condition.wait(timeout)
def notify(self):
"""Interrupt the current sleep operation.
If the thread is currently sleeping, it will be woken immediately,
instead of waiting the full duration of the timeout.
If the thread is not sleeping, it will run again as soon as it is
done with its operation.
"""
assert _debug('MediaThread.notify()')
with self._condition:
self._condition.notify()
assert _debug('PlayerWorkerThread.notify()')
self._rest_event.set()
def add(self, player):
"""
Add a player to the PlayerWorkerThread; which will call
`refill_buffer` on it regularly. Notify the thread as well.
Do not call this method from within the thread, as it will deadlock.
"""
assert player is not None
assert _debug('PlayerWorkerThread: player added')
with self._operation_lock:
self.players.add(player)
self.notify()
def remove(self, player):
"""
Remove a player from the PlayerWorkerThread, or ignore if it does
not exist.
Do not call this method from within the thread, as it may deadlock.
"""
assert _debug('PlayerWorkerThread: player removed')
if player in self.players:
with self._operation_lock:
self.players.remove(player)
# self.notify()
@classmethod
def atexit(cls):
with cls._threads_lock:
threads = list(cls._threads)
for thread in threads:
for thread in list(cls._threads):
thread.stop()
# Can't be 100% sure that all threads are stopped here as it is technically possible that
# a thread may just have removed itself from cls._threads as the last action in `run()`
# and then was unscheduled; But it will definitely finish very soon after anyways
atexit.register(MediaThread.atexit)
class PlayerWorkerThread(MediaThread):
"""Worker thread for refilling players."""
# Time to wait if there are players, but they're all full:
_nap_time = 0.05
def __init__(self):
super().__init__()
self.players = set()
def run(self):
while True:
# This is a big lock, but ensures a player is not deleted while
# we're processing it -- this saves on extra checks in the
# player's methods that would otherwise have to check that it's
# still alive.
with self._condition:
assert _debug('PlayerWorkerThread: woke up @{}'.format(time.time()))
if self._stopped:
break
sleep_time = -1
if self.players:
filled = False
for player in list(self.players):
filled = player.refill_buffer()
if not filled:
sleep_time = self._nap_time
else:
assert _debug('PlayerWorkerThread: No active players')
sleep_time = None # sleep until a player is added
if sleep_time != -1:
self.sleep(sleep_time)
else:
# We MUST sleep, or we will starve pyglet's main loop. It
# also looks like if we don't sleep enough, we'll starve out
# various updates that stop us from properly removing players
# that should be removed.
self.sleep(self._nap_time)
def add(self, player):
assert player is not None
assert _debug('PlayerWorkerThread: player added')
with self._condition:
self.players.add(player)
self._condition.notify()
def remove(self, player):
assert _debug('PlayerWorkerThread: player removed')
with self._condition:
if player in self.players:
self.players.remove(player)
self._condition.notify()
atexit.register(PlayerWorkerThread.atexit)

View File

@ -114,8 +114,9 @@ class CodecRegistry:
will be return if no encoders for that extension are available.
"""
if filename:
extension = os.path.splitext(filename)[1].lower()
return self._encoder_extensions.get(extension, [])
root, ext = os.path.splitext(filename)
extension = ext if ext else root # If only ".ext" is provided
return self._encoder_extensions.get(extension.lower(), [])
return self._encoders
def get_decoders(self, filename=None):
@ -124,8 +125,9 @@ class CodecRegistry:
will be return if no encoders for that extension are available.
"""
if filename:
extension = os.path.splitext(filename)[1].lower()
return self._decoder_extensions.get(extension, [])
root, ext = os.path.splitext(filename)
extension = ext if ext else root # If only ".ext" is provided
return self._decoder_extensions.get(extension.lower(), [])
return self._decoders
def add_decoders(self, module):