Как воспроизводить потоковое аудио с помощью pyglet? - PullRequest
0 голосов
/ 13 июня 2018

Цель этого вопроса - попытаться выяснить, как воспроизводить потоковое аудио с помощью pyglet.Во-первых, просто убедитесь, что вы можете воспроизводить mp3-файлы с помощью pyglet, вот цель этого первого фрагмента:

import sys
import inspect
import requests

import pyglet
from pyglet.media import *

pyglet.lib.load_library('avbin')
pyglet.have_avbin = True


def url_to_filename(url):
    return url.split('/')[-1]


def download_file(url, filename=None):
    filename = filename or url_to_filename(url)

    with open(filename, "wb") as f:
        print("Downloading %s" % filename)
        response = requests.get(url, stream=True)
        total_length = response.headers.get('content-length')

        if total_length is None:
            f.write(response.content)
        else:
            dl = 0
            total_length = int(total_length)
            for data in response.iter_content(chunk_size=4096):
                dl += len(data)
                f.write(data)
                done = int(50 * dl / total_length)
                sys.stdout.write("\r[%s%s]" % ('=' * done, ' ' * (50 - done)))
                sys.stdout.flush()


url = "https://freemusicarchive.org/file/music/ccCommunity/DASK/Abiogenesis/DASK_-_08_-_Protocell.mp3"
filename = "mcve.mp3"
download_file(url, filename)

music = pyglet.media.load(filename)
music.play()
pyglet.app.run()

Если вы установили библиотеки pip install pyglet requests, а также установили AVBin На этом этапе вы сможете прослушивать mp3, как только он будет загружен.

Как только мы достигнем этой точки, я бы хотел выяснить, как воспроизвести и буферизовать файл ваналогично большинству существующих веб-видео / аудиоплееров, использующих запросы pyglet +.Это означает воспроизведение файлов без ожидания до полной загрузки файла.

После прочтения носителя pyglet docs вы увидите, что доступны следующие классы:

media
    sources
        base
            AudioData
            AudioFormat
            Source
            SourceGroup
            SourceInfo
            StaticSource
            StreamingSource
            VideoFormat
    player
        Player
        PlayerGroup

Я видел, что есть и другие подобные вопросы SO, но они не были решены должным образом, и их содержание не содержит много соответствующих деталей:

Вот почему я создал новый вопрос.Как вы проигрываете потоковое аудио, используя pyglet?Не могли бы вы привести небольшой пример использования вышеуказанного mcve в качестве базы?

1 Ответ

0 голосов
/ 22 июня 2018

Если вы не хотите импортировать новый пакет, чтобы сделать это для вас - это можно сделать с небольшим усилием.

Сначала давайте перейдем к исходному коду 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 делает. Вы можете использовать пример здесь для воспроизведения любого звука, который вы хотите по ссылке. Если вы не делаете это только для вызова, я настоятельно рекомендую вам использовать другой пакет. В противном случае: удачи.

...