Comment lire l'audio en streaming avec pyglet?

le but de cette question Est d'essayer de comprendre comment jouer l'audio en streaming en utilisant pyglet. La première est de s'assurer que vous êtes en mesure de lire des fichiers mp3 en utilisant pyglet, c'est le but de ce premier extrait:

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()

Si vous avez installé les bibliothèques pip install pyglet requests et aussi installé AVBin à ce stade, vous devriez être en mesure d'écouter les fichiers mp3 une fois qu'il est téléchargé.

une fois que nous aurons atteint ce point, j'aimerais savoir comment jouer & buffering le fichier d'une manière similaire à la plupart des lecteurs web vidéo/audio existants en utilisant les requêtes pyglet+. Cela signifie jouer les fichiers sans attendre jusqu'à ce que le fichier ait été téléchargé complètement.

Après la lecture de la pyglet médias docs vous pouvez voir qu'il y a à disposition de ces classes:

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

j'ai vu qu'il y a d'autres questions similaires, mais elles n'ont pas été résolues correctement et leur contenu ne fournit pas beaucoup de renseignements pertinents. détails:

  • Lecture audio en continu à l'aide de pyglet
  • Comment puis-je lire le flux audio sans le sauvegarder dans le fichier avec pyglet?

C'est pourquoi j'ai créé une nouvelle question. Comment lire l'audio en streaming avec pyglet? Pourriez-vous fournir un petit exemple en utilisant la mcve ci-dessus comme base?

15
demandé sur BPL 2018-06-13 00:21:32

1 réponses

en Supposant que vous ne souhaitez pas importer un nouveau package pour le faire pour vous - cela peut être fait avec un peu d'effort.

tout d'abord, dirigeons-nous vers le code source de Pyglet et regardons media.loadmedia/__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()

La ligne critique est ici loaded_source = decoder.decode(...). Essentiellement, pour charger Audio Pyglet prend un dossier et le transporte à un décodeur de médias (par exemple. FFMPEG), qui renvoie alors une liste de 'frames' ou de paquets que Pyglet peut jouer avec unPlayer classe. Si le format audio est comprimé (p. ex. mp3 ou aac), Pyglet utilisera une bibliothèque externe (actuellement seule AVBin est prise en charge) pour la convertir en brut, audio décompressé. Vous le savez probablement déjà, certains de cela.

donc si nous voulons voir comment nous pouvons bourrer un flux d'octets dans le moteur audio de Pyglet plutôt qu'un fichier, nous aurons besoin de jeter un oeil à l'un des décodeurs. Pour cet exemple, utilisons FFMPEG car C'est le plus facile d'accès.

Dans 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))

L'objet dont il hérite est MediaDecoder, trouvé dans media/codecs/__init__.py. De retour à l' load function media/__init__.py, vous verrez que pyglet choisira un MediaDecoder basé sur l'extension de fichier, puis retournera son decode function avec le fichier comme paramètre pour obtenir l'audio sous la forme d'un flux de paquets. Que de flux de paquets est un Source objet; chaque décodeur a sa propre saveur, sous la forme de StaticSource ou StreamingSource. Le premier est utilisé pour stocker l'audio en mémoire, et le dernier à jouer immédiatement. Le décodeur de FFmpeg ne supporte que StreamingSource.

nous pouvons voir que FFMPEG's est FFmpegSource, également situé dans media/codecs/ffmpeg.py. Nous trouvons ce Goliath d'une classe:

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()

La ligne que vous serez intéressé par ici self._file = ffmpeg_open_filename(asbytes_filename(filename)). Cela nous amène ici, une fois de plus en 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

et c'est là que les choses deviennent confuses: il appelle à une ctypes fonction (avformat_open_input) que lorsqu'un fichier, va récupérer ses détails et remplissez toutes les informations dont il a besoin pour notre classe FFmpegSource. Avec un peu de travail, vous devriez être en mesure d'obtenir avformat_open_input pour prendre un objet bytes plutôt qu'un chemin vers un fichier qu'il ouvrira pour obtenir les mêmes informations. J'aimerais bien faire et d'inclure un exemple de travail, mais je n'ai pas le temps maintenant. Vous devez alors créer une nouvelle fonction ffmpeg_open_filename en utilisant la nouvelle fonction avformat_open_input, puis une nouvelle classe FFmpegSource en utilisant la nouvelle fonction ffmpeg_open_filename. Tout ce dont vous avez besoin maintenant est une nouvelle classe FFmpegDecoder utilisant la nouvelle classe FFmpegSource.

vous pouvez alors implémenter ceci en l'ajoutant directement à votre paquet pyglet. Après, vous voulez ajouter le support d'un argument byte object dans la fonction load () (situé dans media/__init__.py et outrepassez le décodeur de votre nouveau. Et là, vous seriez maintenant en mesure de stream audio sans le sauvegarder.





Ou, vous pouvez simplement utiliser un package qui supporte déjà. Python-vlc ne. Vous pouvez utiliser l'exemple ici pour lire l'audio que vous voulez à partir d'un lien. Si vous ne faites pas cela juste pour un défi, je vous recommandons vivement d'utiliser un autre paquet. Sinon: bonne chance.

6
répondu jman005 2018-06-21 21:33:17