Ответ 1
Предполагая, что вы не хотите импортировать новый пакет, чтобы сделать это для вас - это можно сделать с небольшими усилиями.
Сначала перейдите к исходному коду Pyglet и посмотрите media.load
в media/__init__.py
.
"""Load a Source from a file.
All decoders that are registered for the filename extension are tried.
If none succeed, the exception from the first decoder is raised.
You can also specifically pass a decoder to use.
:Parameters:
'filename' : str
Used to guess the media format, and to load the file if 'file' is
unspecified.
'file' : file-like object or None
Source of media data in any supported format.
'streaming' : bool
If 'False', a :class:'StaticSource' will be returned; otherwise
(default) a :class:'~pyglet.media.StreamingSource' is created.
'decoder' : MediaDecoder or None
A specific decoder you wish to use, rather than relying on
automatic detection. If specified, no other decoders are tried.
:rtype: StreamingSource or Source
"""
if decoder:
return decoder.decode(file, filename, streaming)
else:
first_exception = None
for decoder in get_decoders(filename):
try:
loaded_source = decoder.decode(file, filename, streaming)
return loaded_source
except MediaDecodeException as e:
if not first_exception or first_exception.exception_priority < e.exception_priority:
first_exception = e
# TODO: Review this:
# The FFmpeg codec attempts to decode anything, so this codepath won't be reached.
if not first_exception:
raise MediaDecodeException('No decoders are available for this media format.')
raise first_exception
add_default_media_codecs()
Критическая строка здесь - loaded_source = decoder.decode(...)
. По существу, для загрузки аудио Pyglet берет файл и переносит его на медиадекодер (например, FFMPEG), который затем возвращает список "кадров" или пакетов, которые Pyglet может играть со встроенным классом Player
. Если формат аудио сжат (например, mp3 или aac), Pyglet будет использовать внешнюю библиотеку (в настоящее время поддерживается только AVBin), чтобы преобразовать ее в необработанный, декомпрессированный звук. Вы, наверное, уже знаете об этом.
Поэтому, если мы хотим увидеть, как мы можем записать поток байтов в звуковой движок Pyglet, а не файл, нам нужно взглянуть на один из декодеров. В этом примере позвольте использовать FFMPEG, поскольку это самый простой доступ.
В media/codecs/ffmpeg.py
:
class FFmpegDecoder(object):
def get_file_extensions(self):
return ['.mp3', '.ogg']
def decode(self, file, filename, streaming):
if streaming:
return FFmpegSource(filename, file)
else:
return StaticSource(FFmpegSource(filename, file))
"Объект", который он наследует, - это MediaDecoder
, найденный в media/codecs/__init__.py
. Вернемся к функции load
в media/__init__.py
, вы увидите, что pyglet выберет MediaDecoder на основе расширения файла, а затем вернет свою функцию decode
с файлом в качестве параметра, чтобы получить звук в виде потока пакетов. Этот поток пакетов является объектом Source
; каждый декодер имеет свой собственный вкус, в виде StaticSource или StreamingSource. Первый используется для хранения аудио в памяти, а второй - для немедленного воспроизведения. Декодер FFmpeg поддерживает только StreamingSource.
Мы видим, что FFMPEG является FFmpegSource, также находящимся в media/codecs/ffmpeg.py
. Мы находим этого Голиафа класса:
class FFmpegSource(StreamingSource):
# Max increase/decrease of original sample size
SAMPLE_CORRECTION_PERCENT_MAX = 10
def __init__(self, filename, file=None):
if file is not None:
raise NotImplementedError('Loading from file stream is not supported')
self._file = ffmpeg_open_filename(asbytes_filename(filename))
if not self._file:
raise FFmpegException('Could not open "{0}"'.format(filename))
self._video_stream = None
self._video_stream_index = None
self._audio_stream = None
self._audio_stream_index = None
self._audio_format = None
self.img_convert_ctx = POINTER(SwsContext)()
self.audio_convert_ctx = POINTER(SwrContext)()
file_info = ffmpeg_file_info(self._file)
self.info = SourceInfo()
self.info.title = file_info.title
self.info.author = file_info.author
self.info.copyright = file_info.copyright
self.info.comment = file_info.comment
self.info.album = file_info.album
self.info.year = file_info.year
self.info.track = file_info.track
self.info.genre = file_info.genre
# Pick the first video and audio streams found, ignore others.
for i in range(file_info.n_streams):
info = ffmpeg_stream_info(self._file, i)
if isinstance(info, StreamVideoInfo) and self._video_stream is None:
stream = ffmpeg_open_stream(self._file, i)
self.video_format = VideoFormat(
width=info.width,
height=info.height)
if info.sample_aspect_num != 0:
self.video_format.sample_aspect = (
float(info.sample_aspect_num) /
info.sample_aspect_den)
self.video_format.frame_rate = (
float(info.frame_rate_num) /
info.frame_rate_den)
self._video_stream = stream
self._video_stream_index = i
elif (isinstance(info, StreamAudioInfo) and
info.sample_bits in (8, 16) and
self._audio_stream is None):
stream = ffmpeg_open_stream(self._file, i)
self.audio_format = AudioFormat(
channels=min(2, info.channels),
sample_size=info.sample_bits,
sample_rate=info.sample_rate)
self._audio_stream = stream
self._audio_stream_index = i
channel_input = avutil.av_get_default_channel_layout(info.channels)
channels_out = min(2, info.channels)
channel_output = avutil.av_get_default_channel_layout(channels_out)
sample_rate = stream.codec_context.contents.sample_rate
sample_format = stream.codec_context.contents.sample_fmt
if sample_format in (AV_SAMPLE_FMT_U8, AV_SAMPLE_FMT_U8P):
self.tgt_format = AV_SAMPLE_FMT_U8
elif sample_format in (AV_SAMPLE_FMT_S16, AV_SAMPLE_FMT_S16P):
self.tgt_format = AV_SAMPLE_FMT_S16
elif sample_format in (AV_SAMPLE_FMT_S32, AV_SAMPLE_FMT_S32P):
self.tgt_format = AV_SAMPLE_FMT_S32
elif sample_format in (AV_SAMPLE_FMT_FLT, AV_SAMPLE_FMT_FLTP):
self.tgt_format = AV_SAMPLE_FMT_S16
else:
raise FFmpegException('Audio format not supported.')
self.audio_convert_ctx = swresample.swr_alloc_set_opts(None,
channel_output,
self.tgt_format, sample_rate,
channel_input, sample_format,
sample_rate,
0, None)
if (not self.audio_convert_ctx or
swresample.swr_init(self.audio_convert_ctx) < 0):
swresample.swr_free(self.audio_convert_ctx)
raise FFmpegException('Cannot create sample rate converter.')
self._packet = ffmpeg_init_packet()
self._events = [] # They don't seem to be used!
self.audioq = deque()
# Make queue big enough to accomodate 1.2 sec?
self._max_len_audioq = 50 # Need to figure out a correct amount
if self.audio_format:
# Buffer 1 sec worth of audio
self._audio_buffer = \
(c_uint8 * ffmpeg_get_audio_buffer_size(self.audio_format))()
self.videoq = deque()
self._max_len_videoq = 50 # Need to figure out a correct amount
self.start_time = self._get_start_time()
self._duration = timestamp_from_ffmpeg(file_info.duration)
self._duration -= self.start_time
# Flag to determine if the _fillq method was already scheduled
self._fillq_scheduled = False
self._fillq()
# Don't understand why, but some files show that seeking without
# reading the first few packets results in a seeking where we lose
# many packets at the beginning.
# We only seek back to 0 for media which have a start_time > 0
if self.start_time > 0:
self.seek(0.0)
---
[A few hundred lines more...]
---
def get_next_video_timestamp(self):
if not self.video_format:
return
if self.videoq:
while True:
# We skip video packets which are not video frames
# This happens in mkv files for the first few frames.
video_packet = self.videoq[0]
if video_packet.image == 0:
self._decode_video_packet(video_packet)
if video_packet.image is not None:
break
self._get_video_packet()
ts = video_packet.timestamp
else:
ts = None
if _debug:
print('Next video timestamp is', ts)
return ts
def get_next_video_frame(self, skip_empty_frame=True):
if not self.video_format:
return
while True:
# We skip video packets which are not video frames
# This happens in mkv files for the first few frames.
video_packet = self._get_video_packet()
if video_packet.image == 0:
self._decode_video_packet(video_packet)
if video_packet.image is not None or not skip_empty_frame:
break
if _debug:
print('Returning', video_packet)
return video_packet.image
def _get_start_time(self):
def streams():
format_context = self._file.context
for idx in (self._video_stream_index, self._audio_stream_index):
if idx is None:
continue
stream = format_context.contents.streams[idx].contents
yield stream
def start_times(streams):
yield 0
for stream in streams:
start = stream.start_time
if start == AV_NOPTS_VALUE:
yield 0
start_time = avutil.av_rescale_q(start,
stream.time_base,
AV_TIME_BASE_Q)
start_time = timestamp_from_ffmpeg(start_time)
yield start_time
return max(start_times(streams()))
@property
def audio_format(self):
return self._audio_format
@audio_format.setter
def audio_format(self, value):
self._audio_format = value
if value is None:
self.audioq.clear()
Здесь вас интересует self._file = ffmpeg_open_filename(asbytes_filename(filename))
. Это приводит нас сюда еще раз в media/codecs/ffmpeg.py
:
def ffmpeg_open_filename(filename):
"""Open the media file.
:rtype: FFmpegFile
:return: The structure containing all the information for the media.
"""
file = FFmpegFile() # TODO: delete this structure and use directly AVFormatContext
result = avformat.avformat_open_input(byref(file.context),
filename,
None,
None)
if result != 0:
raise FFmpegException('Error opening file ' + filename.decode("utf8"))
result = avformat.avformat_find_stream_info(file.context, None)
if result < 0:
raise FFmpegException('Could not find stream info')
return file
и здесь все становится беспорядочным: он вызывает функцию ctypes (avformat_open_input), которая при задании файла будет захватывать свои данные и заполнять всю необходимую информацию для нашего класса FFmpegSource. При некоторой работе вы должны получить avformat_open_input, чтобы взять объект байта, а не путь к файлу, который он откроет, чтобы получить ту же информацию. Я бы хотел сделать это и включить рабочий пример, но сейчас у меня нет времени. Затем вам нужно создать новую функцию ffmpeg_open_filename, используя новую функцию avformat_open_input, а затем новый класс FFmpegSource, используя новую функцию ffmpeg_open_filename. Теперь вам нужен новый класс FFmpegDecoder с использованием нового класса FFmpegSource.
Затем вы можете реализовать это, добавив его непосредственно в пакет pyglet. После этого вы захотите добавить поддержку аргумента байтового объекта в функцию load() (расположенную в media/__init__.py
и переопределите декодер на новый. И теперь вы сможете передавать аудио без сохранения Это.
Или вы можете просто использовать пакет, который уже поддерживает его. Python-vlc делает. Вы можете использовать пример здесь, чтобы воспроизвести любой звук, который вы хотели бы получить по ссылке. Если вы не делаете это просто для вызова, я настоятельно рекомендую вам использовать другой пакет. В противном случае: удачи.