Skip to main content

reactor-webrtc (Python)

Python bindings for the reactor-webrtc WebRTC engine, built with PyO3 and distributed as a self-contained wheel — no separate native library required at runtime.

Installation

pip install reactor-webrtc

Requires Python ≥ 3.10.

Quick start

PeerConnection's signaling methods are natively awaitable, so this runs inside an asyncio event loop:

import asyncio
import reactor_webrtc as rw

async def main():
    factory = rw.PeerConnectionFactory()

    obs = rw.PeerConnectionObserver()
    obs.on_ice_candidate = lambda c: relay_to_peer(c)
    obs.on_connection_state_change = lambda s: print("state:", s)

    config = rw.RtcConfiguration(ice_servers=[
        rw.IceServer(urls=["stun:stun.l.google.com:19302"]),
        # A turn:/turns: entry needs both credentials, or libwebrtc rejects the
        # whole configuration.
        rw.IceServer(urls=["turn:turn.example.com:3478"], username="alice", password="secret"),
    ])
    pc = factory.create_peer_connection(config, obs)

    offer = await pc.create_offer()
    await pc.set_local_description(offer)

    # Exchange offer.sdp with the remote peer via your signaling channel, then:
    # await pc.set_remote_description(remote_answer)
    # await pc.add_ice_candidate(candidate)

asyncio.run(main())

Audio

# Headless / server: push PCM programmatically (synthetic ADM, default)
factory = rw.PeerConnectionFactory()
factory.push_audio_frame(pcm_bytes, sample_rate=48000, channels=1)

# Desktop client: real mic + AEC3 + noise suppression
factory = rw.PeerConnectionFactory(
    platform_adm=True,
    echo_canceller=True,
    noise_suppression=True,
)

# Per-peer track, fed directly: pass a capture time so the receiver can line
# this audio up with the video produced alongside it.
audio = factory.create_audio_track_with_local_source("mic")
now = rw.time_micros()
video.push_video_frame(bgra, 320, 240, capture_time_us=now)
audio.push_pcm(pcm_bytes, 48000, 1, capture_time_us=now)

An audio track's RTP timestamp counts the samples it has been handed, so it says how much audio exists but not when it happened — hence capture_time_us, and hence the rule that a producer owes the track a frame (silence, if it has nothing) on every tick. See docs/av-sync.md.

Factory builder + per-track options

Every process-physical choice composes on one builder; every per-track choice arrives as kwargs on the track creation:

builder = rw.PeerConnectionFactoryBuilder()
builder.with_platform_adm()          # real mic + AEC3/NS/AGC/high_pass
builder.with_metadata(False)         # factory-wide frame-metadata kill switch
builder.with_dtls_in_stun(True)      # SPED: DTLS handshake inside the ICE checks
builder.with_receive_playout_delay() # decode each frame as soon as it's complete
factory = builder.build()

# Raw video with an H.264 backend chosen per track:
camera = factory.create_video_track_with_options(
    "cam", h264_backend=rw.H264Backend.VideoToolbox)

# Pre-encoded video (previously with_encoded_video_track's factory):
screen = factory.create_video_track_with_options(
    "screen", pre_encoded=(1920, 1080))

# Encoder feedback: keyframe requests and BWE rate updates:
screen.on_encoder_feedback(
    lambda fb: push_an_idr_soon() if isinstance(fb, rw.KeyFrameRequest)
    else adapt(bitrate=fb.bitrate_bps))

# Music next to the mic — a per-track push pipe:
music = factory.create_audio_track_with_options(
    "music", source=rw.AudioTrackSource.LocalPush,
    echo_cancellation=False, noise_suppression=False)

Pre-encoded video

factory, video = rw.PeerConnectionFactory.with_encoded_video_track(
    "camera", width=1280, height=720
)
pc = factory.create_peer_connection(config, obs)
tx = video.add_transceiver(pc, rw.TransceiverDirection.SendOnly)

# From your encoder thread:
video.push_encoded_frame(
    data=h264_annex_b,
    is_key_frame=True,
    width=1280, height=720,
)

Codec preferences

tx = pc.add_transceiver(rw.MediaKind.Video, rw.TransceiverDirection.SendOnly)
await tx.set_track(video_track)
await tx.set_codec_preferences([rw.VideoCodec.Vp9, rw.VideoCodec.Vp8])

answer = await pc.create_answer()
await pc.set_local_description(answer)

set_local_description/set_remote_description also make this transceiver's own sender actually encode with whichever preferred codec was negotiated, once negotiation completes — not just list it first in the SDP. No further call needed.

Receiving media

obs = rw.PeerConnectionObserver()

def on_track(kind, track):
    if kind == rw.MediaKind.Video:
        track.on_video_frame(lambda bgra, w, h: display(bgra, w, h))
    elif kind == rw.MediaKind.Audio:
        track.on_audio_frame(lambda pcm, sr, ch, n: play(pcm))

obs.on_track = on_track

Stats

report = await pc.get_stats()
for pair in report.candidate_pairs:
    print(pair.state, f"{pair.current_round_trip_time_s * 1000:.1f}ms")

API reference

Class Description
PeerConnectionFactory Entry point; creates peer connections and tracks
PeerConnection SDP offer/answer, ICE, tracks, data channels, stats
PeerConnectionObserver Callbacks: state, ICE candidate, track, data channel
RtcConfiguration ICE servers, ICE transport type, gathering policy
IceServer A STUN or TURN server entry
IceCandidate A trickled ICE candidate
SessionDescription SDP offer or answer (kind, sdp, ice_ufrags, with_ice_credentials, declares_frame_metadata, with_frame_metadata)
FrameMetadata Per-frame frame_id, timestamp, user_data
FrameMetadataGate What the remote declared about per-frame metadata
Track Local (push frames) or remote (attach sink) media track; push_video_frame and push_pcm take an optional capture_time_us
EncodedVideoTrack Push pre-encoded video (H.264 Annex-B, VP8, VP9, …)
Transceiver RTP m-section: mid, kind, set_track, set_direction, set_codec_preferences, set_sender_transform, set_receiver_transform
DataChannel SCTP data channel: send, on_message, on_open, …
StatsReport inbound_rtp, outbound_rtp, candidate_pairs
FrameMetadata, FrameAction, EncodedFrame, FrameTransform Per-frame metadata trailers and custom encoded-frame transforms — see docs/frame-metadata.md
Function Description
time_micros() The engine's monotonic clock in µs — the epoch every capture_time_us is read in, see docs/av-sync.md
Enum Values
PeerConnectionState New, Connecting, Connected, Disconnected, Failed, Closed
IceGatheringState New, Gathering, Complete
TransceiverDirection SendRecv, SendOnly, RecvOnly, Inactive
VideoCodec Vp8, Vp9, Av1, H264, H265
MediaKind Audio, Video
DataChannelState Connecting, Open, Closing, Closed
IceCandidatePairState Waiting, InProgress, Failed, Succeeded, Cancelled
String-valued field Values
RtcConfiguration.ice_transport_type all (default), relay, no_host, none
RtcConfiguration.continual_gathering_policy once (default), continually
RtcConfiguration.bundle_policy Balanced (default), MaxBundle, MaxCompat
RtcConfiguration.tcp_candidate_policy Disabled (default), Enabled

RtcConfiguration also takes a min_port/max_port pair (UDP port range), ice_connection_receiving_timeout_ms, ice_check_interval_strong_connectivity_ms, and sctp_snap (SNAP: SCTP parameters in the SDP, so a data channel opens two round trips sooner — the peer must opt in too); PeerConnectionFactoryBuilder.with_dtls_in_stun is its factory-wide DTLS counterpart (SPED), and PeerConnection.set_bitrate sets congestion-control bitrate limits after the connection is created. All covered in docs/configuration.md.

Per-frame metadata

Arbitrary bytes can ride alongside each encoded video frame, in a protobuf trailer appended to the payload:

video.push_video_frame(bgra, 320, 240, user_data=b"anything you like")

def on_frame(bgra, w, h, meta):
    if meta is not None:
        print(meta.frame_id, meta.timestamp, meta.user_data)

track.on_video_frame(on_frame)

That only works if the far end strips the trailer before its decoder sees it, so support is negotiated in the SDP and you do not have to do anything for it:

  • create_offer advertises the capability as one session-level a=x-reactor-frame-metadata:1 (rw.FRAME_METADATA_ATTRIBUTE, rw.FRAME_METADATA_VERSION).
  • create_answer mirrors an offer that asked for it.
  • set_remote_description arms pc.frame_metadata_gate() and, when it is open, installs the embed and strip transforms on the video transceivers. The sender transform still checks the gate per frame, so a renegotiation that drops support stops the trailers.

A peer that has never heard of the attribute ignores it, the gate stays closed, and user_data is silently dropped rather than corrupting that peer's decode. Read pc.frame_metadata_gate().is_open() if you want to know whether the peer agreed.

Read the declaration from the signalled SDP string, not from pc.remoteDescription or its equivalents: libwebrtc and browsers both discard a= lines they do not recognise while parsing.

A FrameTransform of your own composes with the metadata step rather than displacing it — the library owns libwebrtc's single transformer slot per sender/receiver and runs both. Your callback goes first in both directions, so it sees exactly the bytes that traverse the network.

To keep frame metadata out of a connection entirely:

config = rw.RtcConfiguration(frame_metadata=False)

No a=extmap, no mirroring, no transforms, and user_data is dropped — the connection is indistinguishable from one built before the capability existed.

Choosing your own ICE credentials

libwebrtc generates the ICE ufrag and password itself and offers no setter. If something upstream needs to recognise a session by its ufrag — an edge relay that demultiplexes on it, say — substitute the credentials in the description before setting it locally:

answer = await pc.create_answer()
answer = answer.with_ice_credentials(my_ufrag, my_password)
await pc.set_local_description(answer)

answer.ice_ufrags()  # ["<my_ufrag>", ...] — one per m-section

The local description is what libwebrtc reads the transport's ICE parameters from, so the substituted values are the ones that end up on the wire.

Two things to get right:

  • Order. Setting the local description is what creates the transport and starts gathering, so substituting afterwards acts on nothing.
  • Renegotiation. Changing the credentials between generations is an ICE restart (RFC 8445 §9). On a renegotiation that is not meant to restart ICE, pass the values the session already uses.

Raises if a value is outside RFC 8445's ranges (ufrag 4–256 characters, password 22–256) or contains anything outside ice-char — which is also what stops a newline in a credential from injecting an SDP line.

Thread safety

PeerConnection's signaling methods (create_offer, create_answer, set_local_description, set_remote_description, add_ice_candidate, get_stats, transceivers) plus set_bitrate are natively awaitable — await them directly, no asyncio.to_thread()/executor wrapping needed. They still take a few milliseconds to resolve while the WebRTC engine responds, but that wait happens off the event loop thread, so it never blocks other coroutines. On Transceiver, set_direction, set_track, and set_codec_preferences are the same way.

Every other method (add_track, add_transceiver, create_data_channel, Transceiver.set_sender_transform/set_receiver_transform, and everything on Track/DataChannel) is a fast synchronous call with no native round-trip, and stays a plain function call — no await.

Callbacks fire on WebRTC internal threads with the GIL acquired; keep them fast.

License

Apache-2.0. Upstream WebRTC is BSD-3-Clause + the WebRTC patent grant.

Metadata

Release files for reactor-webrtc 0.21.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distributions (wheels)

Table of built distributions (wheels) for reactor-webrtc 0.21.0
File
reactor_webrtc-0.21.0-cp310-abi3-win_amd64.whl CPython 3.10 abi3 Windows x86-64 Details
reactor_webrtc-0.21.0-cp310-abi3-manylinux_2_34_x86_64.whl CPython 3.10 abi3 Linux glibc 2.34+ x86-64 Details
reactor_webrtc-0.21.0-cp310-abi3-manylinux_2_34_aarch64.whl CPython 3.10 abi3 Linux glibc 2.34+ ARM64 Details
reactor_webrtc-0.21.0-cp310-abi3-macosx_13_0_x86_64.whl CPython 3.10 abi3 macOS 13.0+ x86-64 Details
reactor_webrtc-0.21.0-cp310-abi3-macosx_11_0_arm64.whl CPython 3.10 abi3 macOS 11.0+ ARM64 Details

Total release size: 42.6 MB

Release files / reactor_webrtc-0.21.0-cp310-abi3-win_amd64.whl

Download URL reactor_webrtc-0.21.0-cp310-abi3-win_amd64.whl
Size 8.3 MB
Tags CPython 3.10 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
6df3c0f6432bd60e1d7c23b22fa98e31a364c22c2331a3acbe5cef3d6b519afc
BLAKE2b-256 checksum
How to use checksums
ea74f499d9cf6b4f4055bc3e744ce359a592770c10f15e88c71119bfe01c27b0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

Release files / reactor_webrtc-0.21.0-cp310-abi3-manylinux_2_34_x86_64.whl

Download URL reactor_webrtc-0.21.0-cp310-abi3-manylinux_2_34_x86_64.whl
Size 9.3 MB
Tags CPython 3.10 Linux glibc 2.34+ x86-64 abi3
SHA-256 checksum
How to use checksums
448bc9849a1c1d4fea27da474afd8627b7214999378373f7c7945ddf2b707adf
BLAKE2b-256 checksum
How to use checksums
5ed9762d07ba694331645fbb33937c1372e9466a5768db7433afe43a09d7b1c9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

Release files / reactor_webrtc-0.21.0-cp310-abi3-manylinux_2_34_aarch64.whl

Download URL reactor_webrtc-0.21.0-cp310-abi3-manylinux_2_34_aarch64.whl
Size 8.8 MB
Tags CPython 3.10 Linux glibc 2.34+ ARM64 abi3
SHA-256 checksum
How to use checksums
03edf4d7d1e571e7f9cc572bc3468915add8f66833f5fc325c3244ba42a12d17
BLAKE2b-256 checksum
How to use checksums
65de4c100462ab7c3e31ebb26fff45a103d469ab9c41b69150237136f36b1074
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

Release files / reactor_webrtc-0.21.0-cp310-abi3-macosx_13_0_x86_64.whl

Download URL reactor_webrtc-0.21.0-cp310-abi3-macosx_13_0_x86_64.whl
Size 8.6 MB
Tags CPython 3.10 abi3 macOS 13.0+ x86-64
SHA-256 checksum
How to use checksums
0cb3d844316c44b2c0bb16ab22082e7acbe60a0558f177dcb727717f076c203a
BLAKE2b-256 checksum
How to use checksums
13f9aa134e9d1df5c0a8fd9af08f44ab6ab142aff5bac5d9adc0d58dfc502b7b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

Release files / reactor_webrtc-0.21.0-cp310-abi3-macosx_11_0_arm64.whl

Download URL reactor_webrtc-0.21.0-cp310-abi3-macosx_11_0_arm64.whl
Size 7.6 MB
Tags CPython 3.10 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
a3c7fd2355e8b45e5e3cabdbf3374cc192819b214de787e861257467ac23d9d4
BLAKE2b-256 checksum
How to use checksums
272b220dedafeb22586308fb80d89c2927111b357e8da96d865ba20dd7dfd3df
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page