Skip to content

TeleFuser Stream Server Guide

This guide covers TeleFuser's real-time stream server, which delivers continuous video (and optional audio) over WebRTC connections — as opposed to the batch request-response mode of telefuser serve.


Quick Start

# 1. Install with WebRTC support
pip install -e ".[webrtc]"

# 2. Start the stream server
telefuser stream-serve examples/stream_server/stream_video_replay.py -p 8088 --skip-validation

# 3. Open the WebRTC client demo
python examples/stream_server/webrtc_client_demo.py --server-url http://localhost:8088

Open http://localhost:8090 in a browser, type a prompt, and click Connect to see real-time video.


Stream Modes

TeleFuser stream server supports two interaction modes, both available over WebRTC:

Mode Transport Direction Use Case
Server Push WebRTC (RTP) Server → Client Real-time preview, text-to-video streaming
Bidirectional WebRTC (RTP + DataChannel) Client ↔ Server Interactive generation, keyboard/camera control, speech-to-video

Server Push (WebRTC)

Client                          Server
  │                               │
  │  POST /v1/stream/webrtc/offer │
  │  (SDP offer + prompt)         │
  │──────────────────────────────►│
  │                               │  stream_task() → serve()
  │  SDP answer                   │  on background thread
  │◄──────────────────────────────│
  │                               │
  │  ◄──── RTP video frames ────  │
  │  ◄──── RTP audio frames ────  │  (optional)
  │                               │
  │  DELETE /v1/stream/webrtc/{id}│
  │──────────────────────────────►│  cleanup

The pipeline's serve() method yields chunks containing JPEG-encoded frames. The WebRTC layer decodes them into av.VideoFrame objects and streams them to the browser via RTP at the target frame rate.

Bidirectional (WebRTC)

Client                          Server
  │                               │
  │  pc.createDataChannel("telefuser")
  │  pc.addTransceiver("video", recvonly)
  │  (optionally add camera/mic tracks)
  │                               │
  │  POST /v1/stream/webrtc/offer │
  │  (SDP offer + config)         │
  │──────────────────────────────►│
  │                               │  create_session(config)
  │                               │  pull_chunks(session_id)
  │  SDP answer                   │  start ChunkRouter
  │◄──────────────────────────────│
  │                               │
  │  ──── DataChannel JSON ─────► │  push_chunk(session_id, data)
  │  ◄──── RTP video frames ────  │  ChunkRouter → FrameGeneratorTrack
  │  ◄──── RTP audio frames ────  │  ChunkRouter → AudioGeneratorTrack
  │  ◄──── DataChannel JSON ────  │  ChunkRouter → metadata
  │                               │
  │  ──── Media tracks (opt) ───► │  IncomingVideoRelay / AudioRelay
  │                               │  → push_chunk(session_id, frames)
  │                               │
  │  {"type": "stop"}             │
  │  or DELETE /v1/stream/webrtc/{}│
  │──────────────────────────────►│  close_session + cleanup

The client must create a DataChannel named "telefuser" before generating the SDP offer. The server reuses that single channel for receiving control input and sending metadata output. Video and audio are transported over RTP media tracks in both directions.

ChunkRouter is the server-side fan-out adapter that consumes the pipeline's pull_chunks() generator exactly once and dispatches:

  • frames_b64 → decoded JPEG → av.VideoFrame → pushed to FrameGeneratorTrack (RTP video)
  • audio_b64 → decoded PCM16 → fed to AudioGeneratorTrack (RTP audio)
  • Remaining metadata → serialized as StreamChunkMessage JSON → sent over DataChannel

CLI Usage

telefuser stream-serve <pipeline_file> [OPTIONS]

Options

Flag Default Description
--port, -p 8088 Server port
--host 0.0.0.0 Bind address
--security-level strict Pipeline validation level (strict, standard, permissive)
--skip-validation false Skip pipeline file security checks

Examples

# Start with default settings
telefuser stream-serve examples/stream_server/stream_video_replay.py

# Custom port and host
telefuser stream-serve my_pipeline.py -p 9000 --host 127.0.0.1

# Skip validation (development only)
telefuser stream-serve my_pipeline.py -p 8088 --skip-validation

Creating a Stream Pipeline

A stream pipeline is a Python file that defines a get_service() function returning a service object. The service must implement one of two protocols.

ServerPushService Protocol

For pipelines that accept a single request and stream output continuously.

from __future__ import annotations
from collections.abc import AsyncGenerator

class MyService:
    def start(self) -> None:
        """Called once at server startup. Load models, open files, etc."""
        ...

    def stop(self) -> None:
        """Called at server shutdown. Release resources."""
        ...

    async def serve(self, request: dict) -> AsyncGenerator[dict, None]:
        """Yield output chunks for a single request.

        The request dict contains all fields from the WebRTC offer
        (prompt, task, fps, duration_s, etc.).
        """
        for i in range(10):
            yield {
                "type": "chunk",
                "index": i,
                "frames_b64": [encode_frame(frame)],
                "fps": 24,
            }

def get_service() -> MyService:
    return MyService()

BidirectionalService Protocol

For pipelines that accept continuous input and produce continuous output.

from __future__ import annotations
from collections.abc import AsyncGenerator

class MyBidirectionalService:
    def start(self) -> None: ...
    def stop(self) -> None: ...

    def create_session(self, config: dict) -> str:
        """Create a new session. Return session_id."""
        ...

    def push_chunk(self, session_id: str, chunk: dict) -> None:
        """Push an input chunk into the session."""
        ...

    async def pull_chunks(self, session_id: str) -> AsyncGenerator[dict, None]:
        """Yield output chunks for the session."""
        ...

    def close_session(self, session_id: str) -> None:
        """Close the session and free resources."""
        ...

def get_service() -> MyBidirectionalService:
    return MyBidirectionalService()

Chunk Format

Server-push chunks should contain the following fields:

Field Type Required Description
type str Yes Always "chunk"
index int Yes Chunk sequence number
frames_b64 list[str] Yes Base64-encoded JPEG frames
fps int Yes Target frame rate
num_frames int No Number of frames in this chunk
resolution str No Frame resolution (e.g., "1920x1080")
prompt str No Echo of input prompt
timestamp float No Chunk generation timestamp
audio_b64 str No Base64-encoded PCM16 audio data
audio_sample_rate int No Audio sample rate (default: 48000)
audio_channels int No Number of audio channels (default: 1)

Audio fields are optional. When present, the WebRTC layer creates an audio track alongside the video track.


API Reference

Endpoints Overview

Endpoint Method Mode Description
/v1/stream/webrtc/offer POST Both WebRTC SDP offer/answer exchange
/v1/stream/webrtc/{session_id} DELETE Both Close WebRTC session
/v1/stream/sessions/{session_id} DELETE Both Close session (pipeline + WebRTC)
/v1/stream/sessions/{session_id}/status GET Both Get session status

WebRTC: SDP Offer

POST /v1/stream/webrtc/offer

This endpoint serves both server-push and bidirectional modes. The server auto-detects the mode based on the pipeline's service type.

Request:

{
  "sdp": "<SDP offer string>",
  "type": "offer",
  "session_id": "optional-uuid",
  "task": "t2v",
  "prompt": "A sunset over the ocean",
  "fps": 24,
  "duration_s": 10,
  "config": {}
}
Field Type Required Description
sdp str Yes SDP offer from browser
type str Yes SDP type (usually "offer")
session_id str No Custom session ID (auto-generated if omitted)
task str Yes Task type (e.g. t2v, i2v, bidirectional)
prompt str No Input prompt
fps int No Target video FPS (default: 24)
config dict No Extra config passed to BidirectionalService.create_session()

Additional fields are allowed ("extra": "allow") and forwarded to the pipeline.

Response (200 OK):

{
  "session_id": "abc123",
  "sdp": "<SDP answer string>",
  "type": "answer"
}

Error responses:

Status Condition
400 Invalid SDP or negotiation failure
503 Stream service not running or max sessions reached

WebRTC: Close Session

DELETE /v1/stream/webrtc/{session_id}

Response (200 OK):

{
  "session_id": "abc123",
  "status": "closed"
}

WebRTC: DataChannel Protocol (Bidirectional)

In bidirectional mode, the client-created "telefuser" DataChannel carries JSON messages in both directions.

Client → Server messages:

// Control input (e.g., keyboard, prompt)
{"type": "control", "key": "ArrowUp", "action": "press"}
{"type": "control", "prompt": "new prompt text"}

// Stop session
{"type": "stop"}

Server → Client messages:

// Output chunk metadata (media sent separately via RTP)
{
  "type": "chunk",
  "session_id": "abc123",
  "index": 0,
  "data": {"type": "chunk", "index": 0, "fps": 24, "timestamp": 1714000000.0},
  "timestamp": 1714000000.0
}

// Generation complete
{
  "type": "done",
  "session_id": "abc123",
  "total_chunks": 240,
  "timestamp": 1714000010.0
}

The control message format is pipeline-defined. The examples above show the convention used by ArrowOverlayService. Your pipeline's push_chunk() receives whatever JSON the client sends.

Stream-Specific Health Fields

GET /v1/service/health returns additional fields when stream service is active:

{
  "status": "healthy",
  "stream_ready": true,
  "stream_mode": "server_push",
  "webrtc_active_sessions": 1,
  "webrtc_server_push_sessions": 1,
  "webrtc_bidirectional_sessions": 0,
  "webrtc_max_sessions": 10
}

Stream-Specific Metadata Fields

GET /v1/service/metadata:

{
  "service_type": "stream",
  "stream_mode": "server_push",
  "pipeline_file": "examples/stream_server/stream_video_replay.py",
  "security_level": "STRICT",
  "runner": "StreamPipelineService",
  "webrtc_active_sessions": 0,
  "webrtc_server_push_sessions": 0,
  "webrtc_bidirectional_sessions": 0,
  "webrtc_max_sessions": 10
}

Client Integration

WebRTC: Server Push (JavaScript)

Minimal browser client for server-push mode:

// For LAN: no iceServers needed (or use default STUN)
// For public network: configure STUN + TURN servers
const pc = new RTCPeerConnection({
  iceServers: [
    { urls: "stun:stun.l.google.com:19302" },
    // Add TURN server for production / NAT traversal:
    // { urls: "turn:your-domain.com:3478", username: "user", credential: "pass" },
  ],
});
pc.addTransceiver("video", { direction: "recvonly" });
pc.addTransceiver("audio", { direction: "recvonly" });

const offer = await pc.createOffer();
await pc.setLocalDescription(offer);

const resp = await fetch("http://localhost:8088/v1/stream/webrtc/offer", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    sdp: pc.localDescription.sdp,
    type: pc.localDescription.type,
    task: "t2v",
    prompt: "A cat playing piano",
  }),
});

const answer = await resp.json();
await pc.setRemoteDescription(answer);

pc.ontrack = (event) => {
  document.getElementById("video").srcObject = event.streams[0];
};

WebRTC: Bidirectional (JavaScript)

Full-duplex client with DataChannel for control and optional camera/mic input:

const pc = new RTCPeerConnection();

// 1. Client MUST create DataChannel before generating the offer
const dc = pc.createDataChannel("telefuser");
dc.onopen = () => {
  // Send control messages after channel opens
  dc.send(JSON.stringify({ type: "control", key: "ArrowUp", action: "press" }));
};
dc.onmessage = (evt) => {
  const msg = JSON.parse(evt.data);
  if (msg.type === "done") console.log("Generation complete:", msg.total_chunks, "chunks");
};

// 2. Optionally send camera/mic to the server
// const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
// stream.getTracks().forEach(t => pc.addTrack(t, stream));

// 3. Receive server output video/audio
pc.addTransceiver("video", { direction: "recvonly" });
pc.addTransceiver("audio", { direction: "recvonly" });

pc.ontrack = (evt) => {
  if (evt.track.kind === "video") {
    document.getElementById("video").srcObject = evt.streams[0];
  }
};

// 4. SDP exchange (same endpoint as server-push)
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);

const resp = await fetch("http://localhost:8088/v1/stream/webrtc/offer", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    sdp: pc.localDescription.sdp,
    type: pc.localDescription.type,
    task: "bidirectional",
    prompt: "A dog running",
    config: { fps: 24 },
  }),
});

const answer = await resp.json();
await pc.setRemoteDescription(new RTCSessionDescription({
  sdp: answer.sdp,
  type: answer.type,
}));

// 5. Stop session
// dc.send(JSON.stringify({ type: "stop" }));
// await fetch(`http://localhost:8088/v1/stream/webrtc/${answer.session_id}`, { method: "DELETE" });

Gradio UI

TeleFuser includes a Gradio-based web UI for WebRTC streaming:

# Start the stream server
telefuser stream-serve examples/stream_server/stream_video_replay.py -p 8088

# Launch the Gradio UI (separate terminal)
python webui/stream_app.py --server-url http://localhost:8088 --port 7860

The Gradio UI provides a prompt input, duration slider, and embedded WebRTC video player.


Examples

Video Replay Service

examples/stream_server/stream_video_replay.py — A server-push pipeline that loads a local video file and streams it as frame chunks:

  • Loads video frames and audio with PyAV at start() time
  • Pre-encodes all frames as JPEG (cached, not re-encoded per request)
  • Streams chunks at configurable pacing with optional audio
  • Supports duration_s, realtime, and prompt request parameters
telefuser stream-serve examples/stream_server/stream_video_replay.py -p 8088 --skip-validation

WebRTC Client Demo

examples/stream_server/webrtc_client_demo.py — A standalone HTML page served via Python's HTTP server:

python examples/stream_server/webrtc_client_demo.py --server-url http://localhost:8088 --port 8090

Opens a browser page with video player, prompt input, and connect/stop/unmute buttons.

Arrow Overlay Service (Bidirectional)

examples/stream_server/stream_arrow_overlay.py — A bidirectional pipeline that loads a video and overlays a D-pad HUD based on keyboard input:

  • Implements BidirectionalService protocol with session state management
  • push_chunk() receives {"type": "control", "key": "ArrowUp", "action": "press"} and updates pressed_keys
  • pull_chunks() yields video frames with D-pad overlay drawn based on pressed_keys
  • Loops through video frames indefinitely until session closes
# Start the server
telefuser stream-serve examples/stream_server/stream_arrow_overlay.py -p 8088 --skip-validation

# Start the client (opens browser at localhost:8092)
python examples/stream_server/webrtc_arrow_overlay_demo.py --server-url http://localhost:8088

Press arrow keys in the browser to see the D-pad overlay respond in real time.

Bidirectional Client Demo

examples/stream_server/webrtc_bidirectional_demo.py — A general-purpose bidirectional WebRTC client with:

  • DataChannel for sending prompts and control messages
  • Optional camera and microphone input via media tracks
  • Server output video player and DataChannel message log
python examples/stream_server/webrtc_bidirectional_demo.py --server-url http://localhost:8088

LingBot-World-Fast Streaming

examples/stream_server/stream_lingbot_world_fast.py provides a bidirectional streaming service for LingBot-World-Fast. The service generates video over WebRTC RTP and receives prompts and direction control messages over DataChannel. The current demo page does not capture the browser camera or microphone; LingBot currently outputs video only, no audio.

Model Files

LingBot-World-Fast requires two sets of weights:

Environment Variable Example Description
LINGBOT_WORLD_CHECKPOINT_DIR /storage/model_zoo/Wan2.2-Distill-Models Base weight directory containing VAE, T5 text encoder, and tokenizer
LINGBOT_WORLD_FAST_CHECKPOINT_SUBDIR /storage/model_zoo/lingbot-world-fast LingBot-World-Fast DiT weight directory; can be an absolute path

Start the Server

Recommended launch with two GPUs: DiT on GPU 0, text encoder and VAE on GPU 1. If GPU 1 VRAM is tight, move VAE back to CPU.

TELEFUSER_TURN_SERVER='turn:127.0.0.1:3478' \
TELEFUSER_TURN_USERNAME=telefuser \
TELEFUSER_TURN_CREDENTIAL=your-turn-password \
LINGBOT_WORLD_CHECKPOINT_DIR=/storage/model_zoo/Wan2.2-Distill-Models \
LINGBOT_WORLD_FAST_CHECKPOINT_SUBDIR=/storage/model_zoo/lingbot-world-fast \
LINGBOT_WORLD_CONTROL_TYPE=cam \
LINGBOT_WORLD_MAX_AREA=99840 \
telefuser stream-serve examples/stream_server/stream_lingbot_world_fast.py \
  -p 8088 --host 0.0.0.0 --skip-validation

Wait for the following log line before connecting the browser demo:

Starting stream server on 0.0.0.0:8088

Verify the service is ready:

curl --noproxy '*' http://127.0.0.1:8088/v1/service/health

Start the Browser Demo

When using VS Code Remote SSH to access the server from a laptop browser, browser JavaScript runs locally. In this case, use TURN and forward the remote 3478 port to local 3478.

python examples/stream_server/webrtc_bidirectional_demo.py \
  --server-url http://localhost:8088 \
  --port 8091 \
  --image-path /tmp/lingbot_test_input.png \
  --frame-num 81 \
  --chunk-size 3 \
  --fps 16 \
  --turn-url 'turn:localhost:3478?transport=tcp' \
  --turn-username telefuser \
  --turn-credential your-turn-password \
  --force-turn-relay \
  --ice-gather-timeout-ms 30000 \
  --control-lateral-step 0.25 \
  --control-yaw-step-degrees 12 \
  --no-open

Open in browser:

http://localhost:8091

--image-path is a server-side file path, not the laptop local path. The demo enables proxying by default; the browser only needs to access the demo port. Requests to /v1/stream/webrtc/* are forwarded by the demo process to --server-url.

Without TURN: View on Server Browser

If the browser actually runs on the server side (e.g., Chrome in remote desktop, VNC, or noVNC), you can skip TURN. Do not set TELEFUSER_TURN_* and do not pass --turn-url to the demo.

Server:

env -u TELEFUSER_TURN_SERVER \
-u TELEFUSER_TURN_USERNAME \
-u TELEFUSER_TURN_CREDENTIAL \
LINGBOT_WORLD_CHECKPOINT_DIR=/storage/model_zoo/Wan2.2-Distill-Models \
LINGBOT_WORLD_FAST_CHECKPOINT_SUBDIR=/storage/model_zoo/lingbot-world-fast \
LINGBOT_WORLD_CONTROL_TYPE=cam \
LINGBOT_WORLD_MAX_AREA=99840 \
telefuser stream-serve examples/stream_server/stream_lingbot_world_fast.py \
  -p 8088 --host 0.0.0.0 --skip-validation

Demo:

env -u TELEFUSER_TURN_SERVER \
-u TELEFUSER_TURN_USERNAME \
-u TELEFUSER_TURN_CREDENTIAL \
python examples/stream_server/webrtc_bidirectional_demo.py \
  --server-url http://127.0.0.1:8088 \
  --port 8091 \
  --image-path /tmp/lingbot_test_input.png \
  --frame-num 81 \
  --chunk-size 3 \
  --fps 16 \
  --control-lateral-step 0.25 \
  --control-yaw-step-degrees 12 \
  --no-open

Open on server browser:

http://127.0.0.1:8091

Direction Control

The demo supports keyboard arrow keys and a page D-pad:

Input cam mode meaning
Camera forward
Camera backward
Turn left and strafe left
Turn right and strafe right

Controls only affect chunks that have not yet started generating; chunks already denoising or decoding are not immediately changed. Therefore, it is recommended to hold a direction key early after connecting, rather than clicking near the end of the video.

The following states in the DataChannel log indicate that direction control has been consumed by the server and applied to a generation chunk:

"stage":"control_state"
"stage":"applying_direction_control"

The demo enables Control HUD by default. The HUD overlay appears in the top-left corner of output chunks that received direction control, confirming the control pipeline is active. Once confirmed, you can uncheck the HUD on the page to observe pure model output.

Common control strength parameters:

Parameter Default Description
--control-move-step 0.18 Forward/backward displacement step size
--control-yaw-step-degrees 10.0 Yaw angle per latent frame
--control-lateral-step 0.12 Left/right lateral step size
--show-control-hud / --no-show-control-hud true Whether to overlay direction HUD on controlled chunks

If left/right effects are subtle, increase --control-lateral-step, e.g., 0.25 or 0.3.

cam vs act Control Modes

Mode Input Description
cam poses + intrinsics Camera trajectory control. The server converts arrow keys into camera poses, then builds a 6-channel camera control tensor.
act poses + intrinsics + action Action control. Requires 7-channel action-control weights.

The current demo defaults to cam. If the model weights are camera-control weights, keep:

LINGBOT_WORLD_CONTROL_TYPE=cam
--control-mode cam

Only switch to act when using action-control weights:

LINGBOT_WORLD_CONTROL_TYPE=act
--control-mode act

VRAM and Resolution

LingBot's KV cache grows with frame_num and output resolution. 832×480 at 81 frames approaches the 80 GB H100 VRAM limit. Start with a lower resolution to verify the pipeline:

LINGBOT_WORLD_MAX_AREA=99840  # approximately 416×240

Common tuning parameters:

Parameter Effect
LINGBOT_WORLD_MAX_AREA Reduce output area, significantly lower VRAM usage
--frame-num Reduce total generated frames and latent chunk count
--chunk-size Affect the latent chunk size per generation step

The service currently allows only one LingBot active session. Before reconnecting, click Stop on the demo, or call:

curl -X DELETE http://127.0.0.1:8088/v1/stream/webrtc/<session_id>

If still experiencing OOM, check whether other processes are using the GPU:

nvidia-smi

Configuration

Environment Variables

Variable Default Description
TELEFUSER_WEBRTC_MAX_SESSIONS 10 Maximum concurrent WebRTC sessions (1-100)
TELEFUSER_STUN_SERVERS ["stun:stun.l.google.com:19302"] STUN server URLs (JSON array)
TELEFUSER_TURN_SERVER None TURN server URL (e.g. turn:your-domain.com:3478)
TELEFUSER_TURN_USERNAME None TURN server username
TELEFUSER_TURN_CREDENTIAL None TURN server credential

CORS

CORS middleware is automatically added when stream service is initialized (required for browser-based WebRTC clients). All origins are allowed by default.

Security Levels

Pipeline files are validated before loading. Use --skip-validation only for development:

Level Description
strict Full validation — no dangerous imports, no file system access
standard Moderate — allows common libraries
permissive Minimal checks

Public Network Deployment

When deploying the stream server on a public network (server and client on different networks), WebRTC requires additional configuration for NAT traversal and browser security.

1. HTTPS (Required)

Browsers block WebRTC on non-localhost HTTP pages. You must serve over HTTPS:

# Option A: Let's Encrypt (recommended for production)
certbot certonly --standalone -d your-domain.com

# Option B: Self-signed certificate (development/testing)
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes

# Start with TLS
uvicorn telefuser.service.api.app:app \
  --host 0.0.0.0 --port 443 \
  --ssl-keyfile key.pem --ssl-certfile cert.pem

2. STUN/TURN Servers

STUN discovers the client's public IP (lightweight, free). TURN relays media when direct connections fail (requires bandwidth, self-host recommended).

# Install coturn (popular open-source TURN server)
apt install coturn

Minimal /etc/turnserver.conf:

listening-port=3478
tls-listening-port=5349
realm=your-domain.com
server-name=your-domain.com
fingerprint
lt-cred-mech
user=telefuser:your-secret-password
external-ip=203.0.113.10
min-port=49152
max-port=65535
systemctl enable coturn && systemctl start coturn

Configure TeleFuser to use the TURN server:

export TELEFUSER_TURN_SERVER="turn:your-domain.com:3478"
export TELEFUSER_TURN_USERNAME="telefuser"
export TELEFUSER_TURN_CREDENTIAL="your-secret-password"
telefuser stream-serve pipeline.py -p 8000

3. Firewall Ports

Port Protocol Purpose
443 TCP HTTPS (API + SDP signaling)
3478 TCP+UDP STUN/TURN
5349 TCP TURN over TLS
49152-65535 UDP TURN relay media ports

Deployment Architecture

┌─────────┐     HTTPS/443      ┌──────────────┐
│  Client  │◄──────────────────►│  Nginx/CDN   │
│ (Browser)│                    │  (TLS term.) │
│          │     UDP            ├──────────────┤
│          │◄──────────────────►│  TeleFuser   │
│          │   (WebRTC media)   │  :8000       │
│          │                    ├──────────────┤
│          │◄──────────────────►│  coturn      │
│          │   UDP 3478 +       │  (TURN relay)│
│          │   49152-65535      │              │
└─────────┘                    └──────────────┘

LAN deployment requires no STUN/TURN configuration. The default settings work out of the box when server and client are on the same network.


Troubleshooting

WebRTC Connection Fails (ICE Error)

For LAN: ensure the server binds to 0.0.0.0 (not 127.0.0.1) and no firewall blocks UDP.

For public network:

  1. Verify STUN/TURN servers are configured (see Public Network Deployment)
  2. Ensure TURN server ports (3478, 49152-65535) are open
  3. Test TURN connectivity: turnutils_uclient -u user -w pass your-domain.com
  4. Check browser DevTools → chrome://webrtc-internals for ICE candidate details

No Audio in Browser

Click the Unmute button. Browsers require a user gesture before playing audio. The video element starts muted by default.

Port Already in Use

# Find and kill the process using the port
lsof -ti:8088 | xargs kill -9

"Stream service is not running" (503)

The pipeline's start() method failed. Check server logs for errors (e.g., missing video file, import errors).

High Memory Usage

Each WebRTC session holds video frames in a queue. Limit webrtc_max_sessions and ensure pipelines don't buffer excessive frames. The server automatically cleans up sessions when clients disconnect.