Skip to main content

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.

Terminal window
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:

FieldWhere to storeNotes
streamIdDB — streams tableYour primary handle for API calls
edgeIdDB — streams tableSafe to expose publicly; used in playback URL
streamKeyDB — encrypted, or KMSShown once. Never log it.
playback.hlsUrlDB — streams tableGive this to viewers

Stream key visibility: streamKey and rtmpFullUrl are 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 route
app.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 → ended

Poll 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:

StateWhat it means for your UI
idleStream exists, no encoder connected. Show “not live” badge.
connectingEncoder connected, transcoder starting (~5–10 s). Show “starting…” spinner.
liveHLS segments are available. Show player + “LIVE” badge.
endedEncoder disconnected, stream finished. Hide player or show replay prompt.
errorUnexpected 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:

Terminal window
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 live or connecting. Check the state first, or handle the 409 Conflict response.

1.5 — Delete a Stream

Terminal window
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:

  1. Settings → Stream → Service: Custom…
  2. Server: rtmp://ingest.antcdn.net/live
  3. Stream Key: <their stream key>

Recommended OBS video settings:

SettingValue
Encoderx264
Rate controlCBR
Bitrate4000–6000 kbps (for standard quality)
Keyframe interval2 s
ProfileHigh
Resolution1280×720 (for standard), 1920×1080 (for high)
FPS30

2.2 — Firewall Requirements

ProtocolPortDirection
RTMP1935 TCPOutbound from encoder

Part 3: Viewer Playback

3.1 — The Playback URL

https://worker.antcdn.net/live/{edgeId}/master.m3u8

This 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.

Terminal window
npm install hls.js
import 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 presetRenditions
standard720p, 480p, 360p + audio
high1080p, 720p, 480p + audio
passthroughSource 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 stream
export 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 only
export 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 AntCDN
export 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

app/dashboard/streams/[id]/page.tsx
"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 idleconnecting 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 caseRecommended preset
Gaming, talk shows, general contentstandard (720p)
High-production events, sportshigh (1080p)
Re-streaming at source qualitypassthrough

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 → store streamId, edgeId, hlsUrl, encrypted streamKey
  • Authenticated endpoint to serve streamKey to 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