bundle.discord.cogs.music.source

Buffered Opus audio source with jitter buffer for stable playback.

Discord.py’s AudioPlayer loop calls source.read() every 20 ms and sends the resulting frame over UDP. When the source is an FFmpeg process streaming from a remote URL, read() may block unpredictably due to network latency or CDN throttling. When a read stalls, the AudioPlayer compensates by sending subsequent frames faster to “catch up”, producing audible speed-up / slow-down artefacts (see discord.py #9120).

This module solves the problem by inserting a jitter buffer between FFmpeg and the AudioPlayer:

[YouTube URL] ──▶ FFmpeg ──▶ _fill() thread ──▶ Queue ──▶ read() ──▶ AudioPlayer

  • A background thread reads Opus frames from FFmpeg into a bounded queue as fast as FFmpeg produces them.

  • read() pops from the queue, which is near-instant as long as the buffer has frames — so the AudioPlayer’s 20 ms cadence is never disrupted.

  • Before playback starts, make_source() blocks until the queue has pre-filled PREFILL_FRAMES (~1 s), eliminating the initial burst that causes the first-seconds speed-up.

FFmpeg flags

before_options (input side):
-reconnect / -reconnect_streamed / -reconnect_delay_max

Auto-reconnect on transient HTTP failures.

-analyzeduration 0 -probesize 32768

Skip lengthy format probing — we already know it’s audio.

-thread_queue_size 4096

Large input-thread packet queue inside FFmpeg itself.

options (output side):
-vn

Strip any video track.

-b:a 256k Opus bitrate (Discord supports up to 384 kbps). -bufsize 5M Encoder rate-control buffer. -application audio Use libopus MDCT mode optimised for music rather

than the default voice/low-delay mode.

Attributes

Classes

BufferedOpusAudio

Jitter buffer that decouples FFmpeg pipe reads from the 20 ms playback loop.

Functions

make_source(→ BufferedOpusAudio)

Create a buffered FFmpeg Opus source from a stream URL.

Module Contents

bundle.discord.cogs.music.source.log
bundle.discord.cogs.music.source.FFMPEG_BEFORE_OPTIONS = '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5 -analyzeduration 0 -probesize 32768...
bundle.discord.cogs.music.source.FFMPEG_OPTIONS = '-vn -b:a 256k -bufsize 5M -application audio'
bundle.discord.cogs.music.source.JITTER_BUFFER_SIZE = 250
bundle.discord.cogs.music.source.PREFILL_FRAMES = 250
class bundle.discord.cogs.music.source.BufferedOpusAudio(source: discord.FFmpegOpusAudio)

Bases: discord.AudioSource

Jitter buffer that decouples FFmpeg pipe reads from the 20 ms playback loop.

Wraps any discord.FFmpegOpusAudio source with a bounded frame queue. A daemon thread (_fill) continuously reads Opus frames from FFmpeg and enqueues them. The read() method — called by discord.py’s AudioPlayer every 20 ms — simply dequeues the next frame, returning near-instantly regardless of upstream network conditions.

Lifecycle:

source = BufferedOpusAudio(ffmpeg_opus_source)
source.wait_ready()   # block until PREFILL_FRAMES are buffered
vc.play(source, ...)  # hand off to discord.py
...
source.cleanup()      # signals the reader thread and cleans up FFmpeg
Parameters:

source – An already-constructed discord.FFmpegOpusAudio instance.

wait_ready(timeout: float = 10.0) bool

Block until the pre-fill target is reached. Returns True if ready.

read() bytes

Return the next Opus frame from the buffer.

Called by discord.py’s AudioPlayer every ~20 ms. Returns an empty bytes when the stream has ended and the buffer is drained, which signals the AudioPlayer to stop and fire the after callback.

is_opus() bool

Tell discord.py that frames are already Opus-encoded.

cleanup() None

Signal the reader thread to stop and release FFmpeg resources.

bundle.discord.cogs.music.source.make_source(stream_url: str) BufferedOpusAudio

Create a buffered FFmpeg Opus source from a stream URL.

Blocks until the jitter buffer has pre-filled (~1 s of audio) so playback starts smoothly without initial speed fluctuations.