Files
2026-06-08 12:02:00 +02:00

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 avc1 H.264 video track and one mp4a AAC 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.py and events.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.