Live streaming, step by step
For conferences, launches, matches, town halls and classes: go live from an encoder or from a web page, let viewers watch on your site or from a link, send the stream on to other platforms, and keep the recording as a video in your library. This page takes you from nothing to a finished broadcast and its replay. Every rule and field is in the reference: Live streaming, Live errors and the API reference.
You need: a plan that includes video (Plans and prices) and, to go live from a web page or to put the stream on your own site, your backend running one of plycdn's server packages (ASP.NET Core, Node.js or Python). To only broadcast from OBS and send viewers a link, the dashboard is enough.
1How it works
- A live input is a standing place to broadcast to, in one video library: "Main stage", "Sunday service". It has a stream key, the secret an encoder publishes with.
- You go live to the input from an encoder (OBS and others) or from a web page.
- Viewers watch through a link you share, or through the player on your own site. Your backend decides who may watch.
- When the broadcast ends, it becomes an ordinary video in the same library, at the same address, and can be made searchable.
Live captions, restreaming and ad insertion come with your plan when it includes them, and content protection with a protection setup on your account. Account → Usage → Plan shows what yours includes, with Live streaming limits; the stream's Settings say so beside each one, and one your plan leaves out says how to ask for it (contact us).
2Create a live stream
In the dashboard: Live streaming → Live streams → New live stream. Give it a name and choose where it lives: a new library made for it, or an existing video library. Choose the delivery (see the next section) and the top quality, then create it. Its page opens with the tabs Overview, Go live, Preview, Share, Streams, Destinations, Settings and Keys.
From your code: creating a live input needs an admin API key, so it belongs in your backend.
POST /api/content/v1/libraries/{libraryId}/live-inputs
{"name": "Main stage", "settings": {"delivery": "broadcast", "quality": "hd"}}
ASP.NET Core
var input = await videos.CreateLiveInputAsync(orgId, library.Id, new CreateLiveInputRequest("Main stage",
new LiveInputSettings { Delivery = DeliveryMode.Broadcast, Quality = "hd" }));
await secrets.SaveAsync(quot;plycdn-live-{input.Id}", input.StreamKey); // shown here, and when revealed or replaced
Node.js
const input = await video.createLiveInput(orgId, libraryId, { name: 'Main stage', settings: { delivery: 'broadcast', quality: 'hd' } });
await secrets.save(`plycdn-live-${input.id}`, input.streamKey);
Python
live_input = video.create_live_input(org_id, library_id, {"name": "Main stage", "settings": {"delivery": "broadcast", "quality": "hd"}})
secrets.save(f"plycdn-live-{live_input['id']}", live_input["streamKey"])
The answer carries the stream key and the publishing addresses (ingest.rtmps, ingest.rtmp, ingest.srt,
ingest.whip). Keep the key in your secret store like a password: out of source control, logs and any page a browser
loads. You can show it again, or replace it, under the stream's Keys tab (every reveal is recorded in the Audit
log). Replacing it stops the old key at once.
Settings you may want now (all optional, all in the stream's Settings tab; a change applies from the next stream):
| Setting | Values | What it does |
|---|---|---|
quality |
source, sd, hd, fhd |
The top quality offered to viewers (default hd, up to 720p). Never above what the encoder sends |
recording |
true, false |
Keep each stream as a video in the library (default on) |
reconnectWindowSeconds |
0 to 600 | How long a dropped encoder may take to come back and carry on the same stream (default 60) |
maxDurationSeconds |
60 up to your cap | The longest one stream runs (default 24 hours) |
3Choose how viewers receive it
| Broadcast (default) | Real-time | |
|---|---|---|
| Delay behind the camera | about 2 to 4 seconds | under 1 second |
| Audience | any size | up to 10,000 by default; the rest are given broadcast automatically |
| Best for | keynotes, matches, services, webinars | auctions, quizzes, live questions, two-way town halls |
| Price | stream minutes plus delivery | stream minutes plus a price per viewer-minute |
Set it with delivery (Delivery in the stream's Settings tab) and change it between streams. For broadcast,
latencyTarget is lowest (as close to live as possible, the default) or balanced (a little further back and steadier
on weak networks). Your encoder's own buffering adds to the delay: use a keyframe every 1 to 2 seconds. More in
Choosing a delivery mode and Latency.
4Go live from an encoder
Any standard encoder works: OBS Studio, vMix, Wirecast, a hardware encoder or a phone app. Open the stream's Go live tab for the addresses with your key already in place; use RTMPS wherever the encoder supports it.
| Protocol | Address | Use it for |
|---|---|---|
| RTMPS | ingest.rtmps |
Encrypted; the usual choice |
| RTMP | ingest.rtmp |
Encoders that cannot encrypt |
| SRT | ingest.srt |
Venues, mobile links and long distances; raise latency=120 on a poor link |
In OBS Studio:
- Settings → Stream, set Service to Custom….
- Paste the whole RTMPS address, with the key in place of
{streamKey}, into Server. Leave Stream Key empty. - Settings → Output: video encoder H.264, rate control CBR, a bitrate your uplink carries with room to spare (about 4,500 to 6,000 kbps for 1080p30, 2,500 to 4,000 for 720p30), keyframe interval 1 or 2 seconds, and audio AAC at 128 kbps, 48 kHz.
- Choose Start Streaming.
Any other encoder: H.264 video, AAC audio, constant bitrate, a keyframe every 1 to 2 seconds, up to 1080p, 12,000 kbps
and 60 frames a second. For SRT in OBS, choose Custom… and paste the ingest.srt address (it starts srt://) into
Server. The dashboard's Go live tab lists the same recommended encoder settings.
An input takes one publisher at a time: a second encoder with the same key is refused (stream_key_in_use).
5Go live from a web page
A browser broadcasts a camera, microphone and shared screen with nothing to install, and never holds the stream key. Your server decides who may go live and the browser receives a short-lived publish token (up to one hour, 15 minutes by default, good for one stream).
1. Decide who may broadcast, on your server. Nobody may until you do this.
ASP.NET Core
public sealed class StageCrewAuthorizer(ICrew crew) : IPlycdnLiveAuthorizer
{
public Task<bool> AuthorizeGoLiveAsync(ClaimsPrincipal user, Guid liveInputId, Guid libraryId, CancellationToken ct) =>
crew.CanBroadcastAsync(user, liveInputId, ct);
}
builder.Services.AddSingleton<IPlycdnLiveAuthorizer, StageCrewAuthorizer>();
Node.js
app.use('/api/plycdn/v1', createPlycdnRouter({
storage, signer: new PlycdnLinkSigner(storage), video: new PlycdnVideoApi(storage),
resolveOrganization: (req) => signedIn(req)?.plycdnOrganizationId ?? null,
authorize: (req, operation, bucketId, key) => true, // your own rules
authorizeGoLive: (req, liveInputId, libraryId) => (signedIn(req)?.roles ?? []).includes('broadcaster'),
}));
Python
def authorize_go_live(request, live_input_id, library_id):
return "broadcaster" in roles_of(request)
app.include_router(plycdn_router(
storage=storage, signer=AsyncPlycdnLinkSigner(storage), video=AsyncPlycdnVideoApi(storage),
resolve_organization=resolve_organization, authorize=authorize, authorize_go_live=authorize_go_live,
), prefix="/api/plycdn/v1")
Being allowed to watch never lets anyone broadcast: going live is decided apart.
2. Show the broadcaster in your page. It asks your server for a token each time it connects or reconnects, shows a preview, camera and microphone choices, a microphone level, Share screen where the browser has it, Go live and End broadcast.
React
import { LiveBroadcaster } from '@plycdn/react/video';
<LiveBroadcaster liveInputId={stage.liveInputId} quality="hd" sources={['camera', 'microphone']}
onStateChange={state => console.log(state.status, state.message)} />
Angular
<plycdn-live-broadcaster [liveInputId]="stage.liveInputId" quality="hd" [sources]="['camera', 'microphone']"
(stateChange)="onBroadcast($event)"></plycdn-live-broadcaster>
The page must be served over https:// (or localhost while you build) and the visitor allows the camera and microphone.
status is idle, preparing, connecting, live, reconnecting, ended or error. To make a token yourself,
CreatePublishTokenAsync, createPublishToken and create_publish_token do it with an admin key; send the token and
whipUrl to the browser, never the key. If your site sends a Content-Security-Policy, add https://*.live.plycdn.com to
connect-src.
6Put the stream on your site
The player asks your backend's sign route for playback links, so your backend decides who may watch (the operation
WatchVideo, with the live input's id). It is the same route the Go live section registered.
React
import { LivePlayer } from '@plycdn/react/video';
<LivePlayer libraryId={stage.libraryId} liveInputId={stage.liveInputId} latencyTarget="lowest" showLatency />
Angular
<plycdn-live-player [libraryId]="stage.libraryId" [liveInputId]="stage.liveInputId"
latencyTarget="lowest" [showLatency]="true" (stateChange)="onLive($event)"></plycdn-live-player>
The player follows the input's delivery (pass mode="broadcast" or "realtime" to choose), shows Waiting for the stream
to start until it is on air, Reconnecting when the encoder drops, a LIVE button that returns to the live edge,
and, when the stream ends, plays the recording in the same place (playRecording off turns that off). A real-time
viewer beyond the audience cap is given broadcast by itself. Links last 15 minutes and are renewed for you.
To sign links yourself (your own player, an app), ask for them with liveInputId in place of videoId:
POST /api/content/v1/sign {"bucketId": "<libraryId>", "liveInputId": "<liveInputId>", "mode": "broadcast"}
SignLiveAsync, signLive and sign_live do it. While nothing is streaming the answer is 409 live_not_started
(retryable): ask again in a few seconds. Give url to the player (fallbackUrl for a native player such as Safari's
or AVPlayer's, at standard delay). Leave IP binding off for live libraries, as phones change networks. Live links take no
tags or hostname (tags_not_on_live, hostname_not_on_live).
7Share a link instead
A share link needs no account, no sign-in and nothing from your backend: send it in an email or chat. Viewers see Starting soon until you are on air, then the stream, then This stream has ended and the recording if you want it shown.
In the dashboard: the stream's Share tab → New link. All settings are optional:
- Name: your own name for the link, to tell links apart (only your team sees it).
- Password (4 to 128 characters): viewers enter it before they can watch. Share it separately.
- Stops working at: from then on the link answers "This link is not available".
- Show the recording after the stream:, the link plays the replay once it is ready (on by default).
An input can have up to 50 links, for example one per audience. A link you turn off, let expire or delete stops at
once, even for people already watching. From your code (admin key): CreateShareLinkAsync, createShareLink and
create_share_link, with UpdateShareLinkAsync/updateShareLink/update_share_link and the matching list and delete calls.
Your look. Account → Branding → Watch page sets the logo, colours, layout, sentences and a button back to your site for every link, with a preview beside the settings; a link can change the title, description, sentences and layout for itself. Step by step: Your watch page; the reference is Brand your watch page.
Your own address. Under Watch page → Watch domains, add a name such as live.example.com (a subdomain, not the
bare domain) and create the two DNS records the page lists. When it shows active, every link also opens on your
address, with the same password, expiry and brand. See
Use your own domain for watch pages.
On your own page. <PlycdnWatch code="https://watch.plycdn.com/…" /> (React) and
<plycdn-watch code="https://watch.plycdn.com/…"> (Angular) show a share link without any route on your backend.
8Send it to YouTube, Facebook and other platforms
Restreaming sends your stream on to other platforms at the same time, when your plan includes it (Restreaming under Account → Usage → Plan).
- On the platform, create a live event and copy its server address (starting
rtmp://orrtmps://) and its stream key. - In the dashboard, the stream's Destinations tab → add a destination: a name, the address (without the key in it)
and the key. From code:
CreateDestinationAsync,createDestinationandcreate_destination. - Choose Test: it checks that the platform answers (nothing is published and the key is not used).
- Go live. Each destination is pushed to while you are on air, and shows idle, connecting, live or failed.
The key is never shown again after you save it. Up to 10 destinations per stream. If one fails three times in five
minutes it shows failed with a reason (destination_unreachable, destination_refused,
destination_rejected_stream, destination_dropped) and your backend gets live.destination_failed. A wrong or
expired key is the usual cause: replace it from Destinations. The address must be rtmp:// or rtmps:// on port
443 or 1935 and must not contain the key (destination_url_invalid). A protected stream is never restreamed.
9Turn on the extras
Each is a setting of the live input, found in the stream's Settings tab; it applies from the next stream. The Resilience, Captions, Ads and Protection groups carry a How it works link to their section below, and say when your plan leaves them out. Full rules are in the reference.
- Rewind (under Rewind and recordings): set a rewind window (
dvrWindowSeconds, up to 7,200 seconds, off by default) and viewers can seek back that far while it is on air. It cannot be combined with ads. Rewinding. - Searchable recordings: Make recordings searchable indexes each recording when it is ready (Searchable recordings).
- Live captions, Ad breaks, Content protection, Passthrough and Backup ingest have their own sections below.
- Your logo and watermark. The live player and the watch page draw your player logo and a watermark of your fixed text and the date and time over the stream, as set in Account → Branding. Live viewers are not named and the watermark is not required on live: Viewer watermark and your logo.
Two settings that cannot be on together answer live_setting_conflict; a setting your plan does not include answers
live_setting_not_in_plan. Backup ingest, captions and the rewind window cannot be changed while a stream runs (live_input_busy).
10Live captions
Captions made from the stream's speech while it is on air, in up to two languages: English and the 22 scheduled languages of India are offered. They appear in the player's captions menu, on the watch page, and stay with the recording.
In the dashboard: the stream's Settings → Captions → turn on Live captions, type up to two Languages
(short codes such as en, hi), choose the Timing, and save. Where your plan leaves captions out, the group says
"Live captions is available on request", with Contact us.
From your code:
PATCH /api/content/v1/live-inputs/{liveInputId}
{"settings": {"captions": {"enabled": true, "languages": ["en", "hi"], "mode": "realtime"}}}
ASP.NET Core
await videos.UpdateLiveInputAsync(orgId, liveInputId, new UpdateLiveInputRequest(Settings: new LiveInputSettings
{
Captions = new LiveCaptionSettings { Enabled = true, Languages = new[] { "en", "hi" }, Mode = CaptionModes.Realtime },
}));
Node.js
await video.updateLiveInput(orgId, liveInputId, { settings: { captions: { enabled: true, languages: ['en', 'hi'], mode: 'realtime' } } });
Python
video.update_live_input(org_id, live_input_id, {"settings": {"captions": {"enabled": True, "languages": ["en", "hi"], "mode": "realtime"}}})
Nothing changes in your pages: <LivePlayer> and <plycdn-live-player> list the languages in their captions menu. Captions
today trail the picture by about half a minute, and the speech is processed in India; a library with strict residency
does not offer them. They are counted by the minute and by language (live_caption_minutes).
Check it works: go live, speak, and open Preview: choose a language in the captions menu.
Problems: 409 live_setting_not_in_plan (detail.setting captions): your plan does not include them. 422 validation_error naming languages: a language the library's region doesn't offer, or more than two. 409 live_input_busy:
a stream is running; change it before the next one. See Live captions.
11Ad breaks
Ads from your own ad server, played to viewers in breaks you start or plan, with your slate where a break has no ad.
1. Switch ads on. The stream's Settings → Ads → turn on Ad insertion, give the Ad server address and its Kind of address (Break schedule (VMAP) or Ad request (VAST)), optionally a Slate (a video shown where a break has no ad) and Personalised ads, and save. Where your plan leaves ads out, the group says "Ad insertion is available on request", with Contact us.
2. Start or plan a break. On the stream's Overview, the Ad breaks panel (shown while ads are on): set the Length (seconds) (up to 600), Starts: Now or At a time (a Date and time up to 24 hours ahead), an optional Break id passed to your ad server, and choose Insert ad break or Plan ad break. Breaks go into a stream that is live: the panel says so until you are on air.
3. Follow and cancel breaks. The same panel lists the stream's breaks with when they start, their length, their state (Planned, Running, Finished, Missed or Cancelled) and their id. A planned break more than 45 seconds away has Cancel.
From your code (admin key):
PATCH /api/content/v1/live-inputs/{liveInputId} {"settings": {"ads": {"enabled": true, "vmapUrl": "https://ads.example.com/vmap?break=[PODID]"}}}
POST /api/content/v1/live-inputs/{liveInputId}/breaks {"durationSeconds": 30, "at": "now", "podId": "midroll-1"}
GET /api/content/v1/live-inputs/{liveInputId}/breaks?limit=20
DELETE /api/content/v1/live-inputs/{liveInputId}/breaks/{breakId}
ASP.NET Core
await videos.UpdateLiveInputAsync(orgId, liveInputId, new UpdateLiveInputRequest(Settings: new LiveInputSettings
{
Ads = new LiveAdsSettings { Enabled = true, VmapUrl = "https://ads.example.com/vmap?break=[PODID]" },
}));
LiveAdBreak now = await videos.InsertAdBreakAsync(orgId, liveInputId, new InsertAdBreakRequest(30));
LiveAdBreak later = await videos.InsertAdBreakAsync(orgId, liveInputId,
InsertAdBreakRequest.Scheduled(60, DateTimeOffset.UtcNow.AddMinutes(20), podId: "midroll-2"));
IReadOnlyList<LiveAdBreak> breaks = await videos.ListAdBreaksAsync(orgId, liveInputId, limit: 20);
await videos.CancelAdBreakAsync(orgId, liveInputId, later.Id);
Node.js
await video.updateLiveInput(orgId, liveInputId, { settings: { ads: { enabled: true, vmapUrl: 'https://ads.example.com/vmap?break=[PODID]' } } });
await video.insertAdBreak(orgId, liveInputId, { durationSeconds: 30 });
const later = await video.insertAdBreak(orgId, liveInputId, { durationSeconds: 60, at: new Date(Date.now() + 20 * 60_000).toISOString(), podId: 'midroll-2' });
const breaks = await video.listAdBreaks(orgId, liveInputId, { limit: 20 });
await video.cancelAdBreak(orgId, liveInputId, later.id);
Python
video.update_live_input(org_id, live_input_id, {"settings": {"ads": {"enabled": True, "vmapUrl": "https://ads.example.com/vmap?break=[PODID]"}}})
video.insert_ad_break(org_id, live_input_id, {"durationSeconds": 30})
later = video.insert_ad_break(org_id, live_input_id, {"durationSeconds": 60, "at": "2026-11-02T10:20:00+05:30", "podId": "midroll-2"})
breaks = video.list_ad_breaks(org_id, live_input_id, limit=20)
video.cancel_ad_break(org_id, live_input_id, later["id"])
A break's state is scheduled, started, ended, missed or cancelled (with cancelledAt); the list is newest first.
Your backend hears live.ad_break when a break starts. Ads cannot be combined with content protection or a rewind window.
Check it works: go live with ads on, insert a 30-second break, and watch Preview: the ads (or your slate) play, then the stream comes back.
Problems:
409 live_not_started: no stream is on air yet; go live first.409 live_input_busy: the break would overlap one already asked for.409 live_cap_reachedwithdetail.capscheduled_breaks: 16 breaks are already waiting on the stream.409 ad_break_not_cancellable: the break has started, ended, was missed or cancelled, or starts within 45 seconds (it is already being prepared);404 ad_break_not_found: no such break on this stream.409 live_setting_conflict: ads with protection or a rewind window.403 feature_not_in_plan: your plan does not include ads.
See Ad breaks and ads.
12Content protection
Protection encrypts the stream and its recording so that only a player that obtains a key can play it, using a protection setup your account has: your own key service, set up with us (contact us). plycdn does not run a key service of its own today. A protected stream is never real-time, restreamed or searchable, and has no ads.
In the dashboard: the stream's Settings → Protection → Content protection: choose one of your account's
protection setups, or Off, add any Protection options the setup needs (one name=value per line), and save. An option that is a credential (a
secret, password, token or key) never takes the credential itself: give the reference to it that we give you when we set
the setup up. A plain value there is refused with 400 protection_not_configured, so no credential is ever kept with the
stream's settings. With no
setup on your account, the group says "Content protection is available on request".
From your code (admin key):
GET /api/content/v1/live/protection-setups → {"items": [{"name": "standard"}]}
PATCH /api/content/v1/live-inputs/{liveInputId} {"settings": {"protection": {"provider": "standard"}}}
ASP.NET Core
IReadOnlyList<ProtectionSetup> setups = await videos.ListProtectionSetupsAsync(orgId);
await videos.UpdateLiveInputAsync(orgId, liveInputId, new UpdateLiveInputRequest(Settings: new LiveInputSettings
{
Protection = new LiveProtectionSettings { Provider = setups[0].Name },
}));
Node.js
const setups = await video.listProtectionSetups(orgId);
await video.updateLiveInput(orgId, liveInputId, { settings: { protection: { provider: setups[0].name } } });
Python
setups = video.list_protection_setups(org_id)
video.update_live_input(org_id, live_input_id, {"settings": {"protection": {"provider": setups[0]["name"]}}})
"provider": "none" switches it off. The live player plays a protected stream by itself; for the recording, turn the live
player's playRecording off and play it in a player of your own that uses the link's protection
(Content protection).
Check it works: go live and open Preview: it plays. The stream's playback link (POST /sign with the
liveInputId) carries protection.
Problems: 400 protection_not_configured: a setup name your account doesn't have, or options it refuses (also 503,
retryable, when a link's key information can't be given). 409 live_setting_conflict: protection with ads.
13Passthrough
When your encoder sends H.264, its own picture can be passed on to viewers as the top quality as it is, instead of being prepared again: the quality viewers see is exactly what you send.
In the dashboard: the stream's Settings → under Quality, Use the source picture when it is H.264 (on by default). Original quality needs it on: the form warns when it is off.
From your code: {"settings": {"passthrough": true}} in PATCH /live-inputs/{liveInputId}
(LiveInputSettings.Passthrough in ASP.NET Core; passthrough in Node.js and Python).
Problems: a stream at Original quality ("quality": "source") whose picture can't be passed on (passthrough off, or
the encoder not sending H.264) ends with input_unsupported: set the encoder to H.264, or choose another quality.
14Backup ingest
For a broadcast that must not fail: a second set of publishing addresses on the same stream key, for a second encoder. If
the main encoder stops sending for about two seconds, the stream carries on from the backup without ending, and your
backend hears live.failover. Redundant processing prepares each stream twice, so that viewers are not interrupted if
one side stops.
In the dashboard: the stream's Settings → Resilience → Backup ingest and Redundant processing. Both are included with live streaming in your plan, and the group says so; where your account leaves one out it says "Available on request". The backup addresses then appear on Overview and under Keys.
From your code: {"settings": {"backupIngest": true, "redundantTranscoding": true}} in
PATCH /live-inputs/{liveInputId}. The answer's ingest gains rtmpsBackup, rtmpBackup and srtBackup: point the
second encoder at one of them, with the same key.
ASP.NET Core
LiveInput input = await videos.UpdateLiveInputAsync(orgId, liveInputId, new UpdateLiveInputRequest(Settings: new LiveInputSettings
{
BackupIngest = true, RedundantTranscoding = true,
}));
Node.js
const input = await video.updateLiveInput(orgId, liveInputId, { settings: { backupIngest: true, redundantTranscoding: true } });
Python
live_input = video.update_live_input(org_id, live_input_id, {"settings": {"backupIngest": True, "redundantTranscoding": True}})
Check it works: start both encoders, then stop the main one: the Preview keeps playing after a short jump, and the stream's Health counts a backup switch.
Problems: 409 live_setting_not_in_plan (detail.setting backupIngest or redundantTranscoding): your account leaves
it out. 409 live_input_busy: a stream is running; change it before the next one. stream_key_in_use: two encoders on
the same address; use the backup address for the second. See
Backup ingest and redundant processing.
15End the stream and use the recording
- Ending. Stop the encoder: if it does not come back within the reconnect window (60 seconds by default), the stream
ends. Viewers see Reconnecting meanwhile. To end it at once, use End stream on the Overview tab, or
POST /live-inputs/{id}/end(EndStreamAsync,endStream,end_stream). To carry on after a drop, reconnect the encoder within the window: it is the same stream. - The recording. With
recordingon, the stream becomes a video with the same id, at the same playback address, so the link that played the live stream plays the replay. Nothing is encoded a second time. It arrives withvideo.playable, thenvideo.ready. Find it in the library under Video streaming → Video libraries. - Searchable. Switch on searchable recordings for the library (Make a video library searchable), or
indexRecordingon the input, and each recording is searchable by what was said when it is ready. See Searchable recordings; it needs a plan with Content Search, and protected or strict-residency recordings are skipped. - Without recording, the stream's files are removed an hour after it ends.
16Watch viewers and health
The stream's Streams tab lists each broadcast with how it ended (endReason), and opens its Viewers (peak, watch
time, top countries) and Health (incoming bitrate, frame rate, keyframe interval, gaps, backup switches). Viewers are
counted, never identified. Broadcast viewers appear when delivery analytics is on for the library. From code (admin
key): GET /streams/{id}/viewers and GET /streams/{id}/health, or GetStreamViewersAsync, getStreamViewers,
get_stream_viewers and the matching health calls.
17Hear about it from your backend
Subscribe a webhook endpoint (Developers → Webhooks) to the live events. Webhooks, step by step shows how to receive and verify them.
| Event | Use it to |
|---|---|
live.connected, live.disconnected |
See the encoder arrive and drop |
live.started |
Show "we are live" on your site |
live.ended |
Publish the replay (data.recording.id), once video.ready follows |
live.failover |
Tell your crew a backup took over |
live.destination_failed |
Tell whoever watches the partner platform to check the key |
live.ad_break |
An ad break started |
Events can arrive more than once and out of order: act once per delivery and read the stream's state from the API when
order matters.
18Safety caps
Each organization has limits that protect you from a runaway encoder, for example 25 streams at once, 1080p, 12,000
kbps, 24 hours a stream and 10 restream destinations per input. Reaching a limit never cuts a running stream: it refuses
the next one (live_cap_reached, with detail.cap). A picture taller than the cap or a bitrate above it for 30
seconds does end the running stream. Ask us to raise any of them; the full table is in Safety caps.
The caps in force for your organization are under Account → Usage → Plan → Live streaming limits, and in the
limits of GET /api/content/v1/plan by the names detail.cap uses (live_concurrent_streams, live_max_height and the
others). The server packages read one by name: plan.Limit(PlanLimits.LiveConcurrentStreams) in ASP.NET Core,
planLimit(plan, PLAN_LIMITS.LiveConcurrentStreams) in Node.js and plan_limit(plan, "live_concurrent_streams") in Python.
These helpers are in the Content Search packages: PlycdnContentApi.GetPlanAsync with PlanLimits in
Plycdn.ContentSearch.AspNetCore, getPlan with planLimit and PLAN_LIMITS from @plycdn/content-search-node, and
get_plan with plan_limit from plycdn_content_search. With the Storage & video packages only, read GET /plan and its
limits by name.
19Check it works
- Create a live stream with a new library, and open its Go live tab.
- Start OBS with the settings above. Within seconds the Overview shows On air; the Preview tab plays it.
- Open the Share tab, create a link and open it in a private window: you see the stream a few seconds behind.
- Stop OBS and wait out the reconnect window (or choose End stream). The link shows This stream has ended and then the replay.
- In Video libraries, the recording is there as an ordinary video.
20Questions and problems
OBS says it cannot connect, or the stream does not appear. Check you pasted the whole address, key included, into
Server and left Stream Key empty. A wrong, replaced or expired key is publish_token_invalid (encoders are simply
disconnected). If the input is disabled (live_input_disabled), switch it on under Settings.
stream_key_in_use. Another encoder is already publishing to the input. Stop it, or use the backup address if you
turned backup ingest on.
The stream ends after a few seconds. input_no_video: the encoder sent sound but no picture within 15 seconds;
add a video source and set a keyframe every 1 to 2 seconds. input_unsupported: the picture is taller than your cap
(1,080 by default) or faster than 60 frames a second. input_bitrate_exceeded: lower the bitrate below 12,000 kbps.
The player says "Waiting for the stream to start" for a long time. The encoder has not gone live yet; it is
live_not_started, not a failure. Players check every 5 seconds.
The player shows nothing on my page. Live streaming may not be switched on yet for your service
(feature_not_available) or your plan (feature_not_in_plan); the player's state event reports the code. Check also
that your Content Security Policy allows https://*.live.plycdn.com and https://*.plycdn.net.
The Go live button in my page is refused. Your authorizer returns false, or none is registered: nobody may go live
until you register one. A used or expired token is publish_token_invalid; the component asks for a new one by itself.
live_input_busy when I change a setting or delete the input. A stream is running. End it first, or wait for the next stream.
live_cap_reached or live_quota_exceeded. A safety cap or your allowance is used. A running stream is not affected;
ask us to raise it.
admin_key_required. Creating inputs, keys, tokens, destinations, share links and the watch page needs an admin key, on your server only.
A restream destination shows failed. Open Destinations, run Test, and check the key on the platform; a
new key starts the destination over.
The share link says "This link is not available". It was turned off, expired or deleted, or the stream cannot be
watched any more (watch_link_not_found). Too many wrong passwords is watch_password_attempts: wait for the time it shows.
There is no recording. The input had recording off, or the stream never went live (it ended before any picture, for
example input_no_video). A live stream's recording cannot be played in a serial-playback library.
Is live streaming available in my library's region? GET /regions says which regions offer live streaming ("live": true);
a library in another region answers region_not_available.