Integration Guide
Live Streaming Integration Guide
This guide walks through building a complete live streaming feature inside your platform — stream lifecycle management, broadcaster flow, viewer playback, and handling edge cases cleanly.
Architecture Overview
Your platform backend AntCDN───────────────────────────────────── ──────────────────────────────────POST /live-streams ──────────► creates stream + stream key │Your broadcaster (OBS / encoder) │ RTMP ──────────────────────────────────►│ ingest │ transcode │ segment upload → R2 │Poll GET /live-streams/{id} ◄──────────────► state: idle → live │ ▼Viewer ◄── HLS master.m3u8 ── Edge worker (worker.antcdn.net)The key insight: your backend manages the stream resource, your broadcaster connects independently, and viewers receive HLS directly from AntCDN’s edge — your servers are never in the media path.
Part 1: Backend Integration
1.1 — Create a Stream
Call this when a user sets up a new channel or show. Store the result in your database.
curl -X POST "https://api.antcdn.net/v1/live-streams" \ -H "X-Api-Key: $ANTCDN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Friday Night Stream", "quality": "standard" }'Response:
{ "streamId": "ls_01jpp...", "edgeId": "lse_01jpp...", "name": "Friday Night Stream", "state": "idle", "quality": "standard", "createdAt": "2026-03-15T10:00:00Z", "ingest": { "rtmpUrl": "rtmp://ingest.antcdn.net/live", "streamKey": "sk_live_abc123...", "rtmpFullUrl": "rtmp://ingest.antcdn.net/live/sk_live_abc123..." }, "playback": { "hlsUrl": "https://worker.antcdn.net/live/lse_01jpp.../master.m3u8", "edgeId": "lse_01jpp..." }}What to store:
| Field | Where to store | Notes |
|---|---|---|
streamId | DB — streams table | Your primary handle for API calls |
edgeId | DB — streams table | Safe to expose publicly; used in playback URL |
streamKey | DB — encrypted, or KMS | Shown once. Never log it. |
playback.hlsUrl | DB — streams table | Give this to viewers |
Stream key visibility:
streamKeyandrtmpFullUrlare only returned on creation and key rotation. After that,GET /live-streams/{id}returns ingest URLs without the key. Retrieve it from your own secure store — you cannot recover it from AntCDN.
1.2 — Deliver Ingest Credentials to Your Broadcaster
Never expose the stream key in a frontend response unless that frontend is your own authenticated dashboard. Serve it through your authenticated API to the signed-in broadcaster only.
// Your API — authenticated routeapp.get("/api/stream/:id/credentials", requireAuth, async (req, res) => { const stream = await db.streams.findById(req.params.id);
// Ensure the requesting user owns this stream if (stream.userId !== req.user.id) return res.status(403).end();
res.json({ rtmpUrl: stream.rtmpIngestUrl, // rtmp://ingest.antcdn.net/live streamKey: decrypt(stream.streamKey), // sk_live_... });});The broadcaster pastes the RTMP server + stream key into OBS.
1.3 — Track Stream State
AntCDN updates the stream state automatically as the encoder connects and disconnects:
idle → connecting → live → endedPoll GET /live-streams/{streamId} every 3–5 seconds to track transitions:
async function pollStreamState(streamId, apiKey, onChange) { let lastState = null;
const interval = setInterval(async () => { const res = await fetch( `https://api.antcdn.net/v1/live-streams/${streamId}`, { headers: { "X-Api-Key": apiKey } }, ); const stream = await res.json();
if (stream.state !== lastState) { lastState = stream.state; onChange(stream.state); }
if (stream.state === "ended" || stream.state === "error") { clearInterval(interval); } }, 4000);
return () => clearInterval(interval); // returns cleanup fn}State meanings:
| State | What it means for your UI |
|---|---|
idle | Stream exists, no encoder connected. Show “not live” badge. |
connecting | Encoder connected, transcoder starting (~5–10 s). Show “starting…” spinner. |
live | HLS segments are available. Show player + “LIVE” badge. |
ended | Encoder disconnected, stream finished. Hide player or show replay prompt. |
error | Unexpected failure. Show error state, offer retry. |
1.4 — Rotate the Stream Key
If a broadcaster’s key is compromised, or they want to invalidate an old streaming session:
curl -X POST "https://api.antcdn.net/v1/live-streams/{streamId}/rotate-key" \ -H "X-Api-Key: $ANTCDN_API_KEY"This immediately disconnects any active encoder using the old key. The response returns the new key in the same one-time format as on creation. Update your encrypted store.
Key rotation is blocked while the stream state is
liveorconnecting. Check the state first, or handle the409 Conflictresponse.
1.5 — Delete a Stream
curl -X DELETE "https://api.antcdn.net/v1/live-streams/{streamId}" \ -H "X-Api-Key: $ANTCDN_API_KEY"Returns 204 No Content. Cannot delete while live or connecting.
Part 2: Broadcaster Flow
2.1 — OBS Studio Setup
Tell your broadcasters to configure OBS like this:
- Settings → Stream → Service:
Custom… - Server:
rtmp://ingest.antcdn.net/live - Stream Key:
<their stream key>
Recommended OBS video settings:
| Setting | Value |
|---|---|
| Encoder | x264 |
| Rate control | CBR |
| Bitrate | 4000–6000 kbps (for standard quality) |
| Keyframe interval | 2 s |
| Profile | High |
| Resolution | 1280×720 (for standard), 1920×1080 (for high) |
| FPS | 30 |
2.2 — Firewall Requirements
| Protocol | Port | Direction |
|---|---|---|
| RTMP | 1935 TCP | Outbound from encoder |
Part 3: Viewer Playback
3.1 — The Playback URL
https://worker.antcdn.net/live/{edgeId}/master.m3u8This is the HLS master playlist. It contains multiple quality renditions (360p, 480p, 720p for standard; adds 1080p for high) plus a separate audio track. Players select quality adaptively.
This URL does not change between streaming sessions — once you’ve given it to a viewer component, it’s valid for the lifetime of the stream resource.
3.2 — HLS.js (cross-browser)
Safari supports HLS natively. All other browsers need hls.js.
npm install hls.jsimport Hls from "hls.js";import { useEffect, useRef } from "react";
interface LivePlayerProps { edgeId: string; state: "idle" | "connecting" | "live" | "ended" | "error";}
export function LivePlayer({ edgeId, state }: LivePlayerProps) { const videoRef = useRef<HTMLVideoElement>(null); const hlsRef = useRef<Hls | null>(null); const src = `https://worker.antcdn.net/live/${edgeId}/master.m3u8`;
useEffect(() => { if (state !== "live") return; const video = videoRef.current; if (!video) return;
if (Hls.isSupported()) { const hls = new Hls({ // Live stream tuning liveSyncDurationCount: 3, // target 3 segments behind live edge liveMaxLatencyDurationCount: 10, manifestLoadingMaxRetry: 10, // keep retrying if segments aren't ready yet fragLoadingMaxRetry: 6, }); hls.loadSource(src); hls.attachMedia(video); hlsRef.current = hls; } else if (video.canPlayType("application/vnd.apple.mpegurl")) { // Safari video.src = src; }
return () => { hlsRef.current?.destroy(); hlsRef.current = null; }; }, [state, src]);
if (state === "idle") { return <div className="stream-placeholder">Stream not started yet</div>; } if (state === "connecting") { return <div className="stream-placeholder">Stream starting…</div>; } if (state === "ended") { return <div className="stream-placeholder">Stream ended</div>; }
return ( <video ref={videoRef} controls autoPlay muted style={{ width: "100%" }} /> );}3.3 — Handling Stream End Mid-Playback
When an encoder disconnects, the HLS stream transitions from live to ended. The HLS playlist gets a final #EXT-X-ENDLIST tag appended within a few seconds. Players stop after draining the buffer.
You should detect this in your UI and update the player state:
hls.on(Hls.Events.ERROR, (event, data) => { // When the live stream ends, hls.js raises a network error // trying to load segments that no longer exist if (data.fatal && data.type === Hls.ErrorTypes.NETWORK_ERROR) { // Re-check stream state from your API checkStreamState().then((state) => { if (state === "ended") setStreamState("ended"); else hls.startLoad(); // try to recover if still live }); }});3.4 — Quality Presets and Renditions
The master playlist contains these renditions depending on the quality setting:
| Quality preset | Renditions |
|---|---|
standard | 720p, 480p, 360p + audio |
high | 1080p, 720p, 480p + audio |
passthrough | Source resolution + audio |
Audio is delivered as a separate track and muxed by the player — this is standard HLS fMP4 behavior and supported by all modern players.
Part 4: End-to-End Platform Example
Here’s a minimal full example tying it all together — a Next.js API route + a React page.
API routes (server-side)
// app/api/streams/route.ts — create a new streamexport async function POST(req: Request) { const { name, quality = "standard" } = await req.json(); const user = await getAuthUser(req);
const res = await fetch("https://api.antcdn.net/v1/live-streams", { method: "POST", headers: { "X-Api-Key": process.env.ANTCDN_API_KEY!, "Content-Type": "application/json", }, body: JSON.stringify({ name, quality }), });
const stream = await res.json();
// Store in your DB — encrypt the stream key await db.streams.create({ userId: user.id, streamId: stream.streamId, edgeId: stream.edgeId, hlsUrl: stream.playback.hlsUrl, streamKeyEncrypted: encrypt(stream.ingest.streamKey), rtmpIngestUrl: stream.ingest.rtmpUrl, });
// Return to client — WITHOUT the stream key return Response.json({ streamId: stream.streamId, edgeId: stream.edgeId, hlsUrl: stream.playback.hlsUrl, state: stream.state, });}// app/api/streams/[id]/credentials/route.ts — serve key to broadcaster onlyexport async function GET( req: Request, { params }: { params: { id: string } },) { const user = await getAuthUser(req); const stream = await db.streams.findById(params.id);
if (!stream || stream.userId !== user.id) { return new Response("Forbidden", { status: 403 }); }
return Response.json({ rtmpUrl: stream.rtmpIngestUrl, streamKey: decrypt(stream.streamKeyEncrypted), });}// app/api/streams/[id]/state/route.ts — proxy state poll to AntCDNexport async function GET( req: Request, { params }: { params: { id: string } },) { const stream = await db.streams.findById(params.id); if (!stream) return new Response("Not found", { status: 404 });
const res = await fetch( `https://api.antcdn.net/v1/live-streams/${stream.streamId}`, { headers: { "X-Api-Key": process.env.ANTCDN_API_KEY! } }, ); const data = await res.json();
return Response.json({ state: data.state, startedAt: data.startedAt });}Broadcaster dashboard page
"use client";import { useEffect, useState } from "react";
export default function StreamDashboard({ params,}: { params: { id: string };}) { const [credentials, setCredentials] = useState<any>(null); const [state, setState] = useState("idle");
// Fetch ingest credentials (stream key) on mount useEffect(() => { fetch(`/api/streams/${params.id}/credentials`) .then((r) => r.json()) .then(setCredentials); }, [params.id]);
// Poll stream state useEffect(() => { const poll = setInterval(async () => { const res = await fetch(`/api/streams/${params.id}/state`); const { state } = await res.json(); setState(state); if (state === "ended" || state === "error") clearInterval(poll); }, 4000); return () => clearInterval(poll); }, [params.id]);
return ( <div> <h1>Your Stream</h1> <p> Status: <strong>{state}</strong> </p>
{credentials && ( <div> <p> RTMP Server: <code>{credentials.rtmpUrl}</code> </p> <p> Stream Key: <code>••••••••</code> <button onClick={() => navigator.clipboard.writeText(credentials.streamKey) } > Copy </button> </p> </div> )} </div> );}Part 5: Edge Cases and Best Practices
Encoder disconnects briefly (reconnect window)
If the encoder drops and reconnects within the reconnect window (default: 60 seconds), the stream resumes automatically without going through idle → connecting again. The HLS playlist stays continuous. Design your state polling to handle this — don’t tear down the player on a brief connecting blip.
// Don't immediately hide the player on 'connecting' if it was just 'live'let wasLive = false;
function handleStateChange(newState) { if (newState === "live") wasLive = true;
if (newState === "connecting" && wasLive) { // Brief reconnect — show "buffering" instead of hiding player setUiState("reconnecting"); } else { setUiState(newState); }
if (newState === "ended") wasLive = false;}Never cache the stream key
Store it encrypted (AES-256-GCM or via a KMS). Log the streamId for debugging, never the key. If you suspect exposure, rotate it immediately.
Use edgeId in playback URLs, never streamId
edgeId is the public identifier safe for client exposure. streamId is the API-level resource handle — keep it server-side.
Gate the player on stream state
Load the HLS source only when state === 'live'. Initializing hls.js before segments exist causes unnecessary retries and noisy error logs.
Multiple reuse of one stream resource
A stream can go idle → live → ended → idle → live → ended repeatedly without creating a new resource. The playback URL stays the same across all sessions. This is the expected pattern for recurring shows — create once, stream many times.
Quality preset selection
| Use case | Recommended preset |
|---|---|
| Gaming, talk shows, general content | standard (720p) |
| High-production events, sports | high (1080p) |
| Re-streaming at source quality | passthrough |
standard encodes three renditions (720p/480p/360p). high adds 1080p. Higher presets use more transcoding resources and increase the warm-up time slightly.
Summary Checklist
Backend setup
-
POST /v1/live-streams→ storestreamId,edgeId,hlsUrl, encryptedstreamKey - Authenticated endpoint to serve
streamKeyto broadcaster only - State polling endpoint proxying
GET /v1/live-streams/{id} - Key rotation endpoint for compromised keys
Broadcaster experience
- Show RTMP server + stream key in broadcaster dashboard
- Document OBS settings (codec: x264, rate: CBR, keyframe: 2s)
- Show stream state in real-time (polling every 3–5 s)
Viewer experience
- Gate player render on
state === 'live' - Show appropriate UI for each state (idle / connecting / live / ended)
- Handle mid-stream encoder disconnect gracefully
- Use hls.js for Chrome/Firefox; native HLS for Safari