4.9 KiB
restream_playout
restream_playout is a self-contained, pure-Python RTMP playout library. It
passes H.264 video and AAC audio from MP4 files directly to an RTMP publishing
endpoint and emits black video with silent audio whenever the queue is empty.
The RTMP publishing session stays open while playout changes between filler and
queued clips.
It does not invoke or depend on FFmpeg, ffprobe, GStreamer, VLC, OBS, MPV, external commands, subprocesses, or native codec libraries. It does not transcode.
Requirements
- Python 3.11 or newer
- A plain
rtmp://publishing endpoint - Input MP4 files with exactly one
avc1H.264 video track and onemp4aAAC audio track - Non-fragmented MP4 with four-byte AVC NAL lengths and a zero-based timeline
Files that cannot be passed through correctly are rejected by enqueue().
Fragmented MP4, edit lists, additional tracks, unsupported codecs, and
encrypted media are outside the pass-through contract.
Install
python -m pip install .
The package has no runtime dependencies outside the Python standard library.
Basic Playout
import logging
from restream_playout import PlayoutEngine
logging.basicConfig(level=logging.INFO)
engine = PlayoutEngine(
rtmp_url="rtmp://live.restream.io/live/STREAM_KEY",
)
engine.on_clip_started(lambda path: logging.info("started %s", path))
engine.on_clip_finished(lambda path: logging.info("finished %s", path))
engine.on_queue_empty(lambda: logging.info("queue empty; filler active"))
engine.on_stream_connected(lambda: logging.info("connected"))
engine.on_stream_disconnected(lambda: logging.warning("disconnected; reconnecting"))
engine.enqueue_many(["intro.mp4", "video1.mp4"])
engine.start()
try:
input("Press Enter to stop\n")
finally:
engine.stop()
start() creates one background thread containing the asyncio playout loop.
enqueue(), enqueue_many(), clear(), and the methods on engine.queue are
safe to call from other threads.
Dynamic Insertion
Calling enqueue() while filler is active switches to the new clip at the next
filler packet boundary without closing the RTMP connection:
engine.start()
engine.enqueue("scheduled.mp4")
# Later, from an API handler, scheduler, or another application thread:
engine.enqueue("breaking-news.mp4")
Queued clips play in FIFO order. Dynamic insertion does not interrupt a clip that is already playing.
Reconnection
StreamSession monitors the RTMP reader and sends periodic RTMP ping requests.
On read, write, or heartbeat failure, the engine reconnects with capped
exponential backoff. After reconnecting it resends metadata plus H.264 and AAC
sequence headers before retrying media delivery.
engine = PlayoutEngine(
"rtmp://live.restream.io/live/STREAM_KEY",
reconnect_min_delay=1.0,
reconnect_max_delay=30.0,
heartbeat_interval=10.0,
)
engine.on_stream_disconnected(lambda: alert("RTMP disconnected"))
engine.on_stream_connected(lambda: alert("RTMP publishing"))
engine.start()
The media timeline remains monotonic across clips, filler transitions, and
reconnections. If network recovery takes longer than max_lag_seconds, wall
clock pacing is rebased so buffered packets are not emitted in a burst.
Architecture
engine.py: synchronous public API, background asyncio loop, pacing, filler transitions, event dispatch, and codec bootstrap handling.session.py: RTMP connection lifecycle, heartbeat, reconnect backoff, and connection generation tracking.rtmp.py: RTMP handshake, AMF0 commands, chunk writer/parser, publish flow, acknowledgements, and ping control messages.mp4.py: ISO BMFF box and sample-table parser for H.264/AAC pass-through.flv.py: AMF0 metadata plus AVC/AAC FLV payload and FLV tag generation.filler.py: Python-generated baseline H.264 black IDR access unit and continuously scheduled AAC silence.queue.pyandevents.py: thread-safe queue and callback dispatch.
The filler video is a valid 16x16 constrained-baseline H.264 stream. It is intentionally tiny because a pure-Python I-PCM encoder is used; this keeps idle CPU and network use low. Each source clip supplies its own codec sequence headers when playout switches.
Operations Notes
- Use a dedicated Restream stream key and protect it like a password.
- Preflight media before scheduling it by calling
parse_mp4(path). - Keep source frame rates, resolutions, H.264 profiles, and AAC layouts consistent for the smoothest player-side transitions.
- Configure logging handlers appropriate for the service environment.
- Run the process under a service manager so process-level failures are restarted. The library handles network/session recovery, not process supervision.
Tests
python -m unittest discover -v
The suite includes a synthetic H.264/AAC MP4 demux test and an in-process RTMP server test covering handshake, command negotiation, publishing, and message delivery.