Live streaming
A live stream is a channel where a small number of people publish and everyone else only watches. The split is decided by the role your server puts in each token, not by anything the client asks for.
Hosts and audience#
POST /v1/token accepts a role of host, cohost or audience. The role determines the permissions baked into the token.
| Role | Can publish | Room admin | Can create the room | Use it for |
|---|---|---|---|---|
host | Yes | Yes | Yes | The broadcaster who starts the stream and can moderate. |
cohost | Yes | No | No | A guest invited on stage. Publishes, but cannot moderate. |
audience | No | No | No | Viewers. Subscribe only. |
Every role can subscribe, and every role can publish data messages, so audience members can still take part in chat and reactions. See data messages.
Audience tokens must use role 'audience'
Hiding the publish button in your UI is not access control. A token issued with host carries publish permission, and anyone who reads that token out of your app can publish audio, video and screen share into your stream. There is no way to take that back without removing the participant.
Only issue host or cohost to users your server has decided are allowed on stage, and re-check that on every token request. A viewer who is invited on stage gets a new token — see promoting a listener, which works the same way for video.
Issuing the two kinds of token
app.post('/api/stream-token', async (req, res) => {const user = await authenticate(req);if (!user) return res.status(401).json({ error: 'Sign in first' });const stream = await db.streams.find(req.body.channel);// YOUR rule for who is allowed on stage.const role = stream.hostUserId === user.id ? 'host': stream.invitedGuests.includes(user.id) ? 'cohost': 'audience';const { token, wsUrl } = await applooma.createToken({roomName: req.body.channel,identity: user.id,role,metadata: { name: user.displayName },});res.json({ token, wsUrl, role });});
Joining as host and as audience#
The client code is the same on both sides except for the role passed to joinChannel. When the role is audience the SDK does not turn on the microphone or camera at all, even if you pass camera: true.
import AppEngine from '@applooma/rtc-web';const engine = AppEngine.create({ appId: 'YOUR_APP_ID' });const { token, wsUrl } = await fetchStreamToken('friday-show');await engine.joinChannel(token, wsUrl, {role: 'host',camera: true,microphone: true,});engine.attachLocalVideo(document.getElementById('preview'));
Audience members who arrive late
The SDK reports participants who were already in the channel when you joined, and delivers their existing tracks. You do not need a separate code path for “the stream already started”.
Bringing in an external encoder#
If the host broadcasts from OBS, a hardware encoder, or any other tool that speaks RTMP or WHIP, create an ingest. The ingest joins your channel as a publishing participant, so the audience sees it exactly like any other host.
POST /v1/streams/ingest — API key auth.
| Field | Type | Notes |
|---|---|---|
roomName | string | Required. Must match [a-zA-Z0-9_-]{1,64}. |
name | string | Optional. Defaults to obs-ingest. Used as the publishing participant's name; its identity becomes ingest-<name>. |
type | string | Optional. rtmp (default) or whip. Any other value is rejected with 400. |
curl -X POST https://api.applooma.dev/v1/streams/ingest \-H "x-applooma-key: ak_your_key" \-H "x-applooma-secret: as_your_secret" \-H "Content-Type: application/json" \-d '{ "roomName": "friday-show", "name": "studio", "type": "rtmp" }'# → 201# {# "ingestId": "IN_xxxxxxxx",# "url": "rtmp://ingest.applooma.dev/x",# "streamKey": "…",# "roomName": "friday-show",# "status": "ready"# }
In OBS, put url in Server and streamKey in Stream Key, then press Start Streaming. For type: "whip" the url is a WHIP endpoint instead, used by browser-based and WebRTC encoders.
Treat the stream key like a password
Anyone holding the URL and key can publish into that channel. Create the ingest on your server, show it only to the host, and delete it when the broadcast is over.
Deleting an ingest
DELETE /v1/streams/ingest/:ingestId
curl -X DELETE https://api.applooma.dev/v1/streams/ingest/IN_xxxxxxxx \-H "x-applooma-key: ak_your_key" \-H "x-applooma-secret: as_your_secret"# → { "deleted": true }# → 404 { "error": "Ingest not found" } if the id belongs to another project
Pushing the channel out to YouTube, Facebook or Twitch#
A media push composites everyone publishing in the channel into a single video and sends it to external RTMP destinations. This is how you simulcast your in-app stream to social platforms.
POST /v1/streams/push — API key auth.
| Field | Type | Notes |
|---|---|---|
roomName | string | Required. The channel to composite. |
urls | string[] | Required. Between 1 and 3 destinations. Each must start with rtmp:// or rtmps:// and include the destination's stream key. |
Collect the destination URLs
Each platform gives you a server URL and a stream key. Join them with a slash into one URL.
- YouTube —
rtmp://a.rtmp.youtube.com/live2/YOUR-KEY - Facebook —
rtmps://live-api-s.facebook.com:443/rtmp/YOUR-KEY - Twitch —
rtmp://live.twitch.tv/app/YOUR-KEY
Start the push
curl -X POST https://api.applooma.dev/v1/streams/push \-H "x-applooma-key: ak_your_key" \-H "x-applooma-secret: as_your_secret" \-H "Content-Type: application/json" \-d '{"roomName": "friday-show","urls": ["rtmp://a.rtmp.youtube.com/live2/YOUR-KEY","rtmp://live.twitch.tv/app/YOUR-KEY"]}'# → 201# { "pushId": "EG_xxxxxxxx", "roomName": "friday-show", "destinations": 2, "status": "active" }
Stop it when the stream ends
POST /v1/streams/push/:pushId/stop
curl -X POST https://api.applooma.dev/v1/streams/push/EG_xxxxxxxx/stop \-H "x-applooma-key: ak_your_key" \-H "x-applooma-secret: as_your_secret"# → { "pushId": "EG_xxxxxxxx", "status": "stopped" }# → 404 { "error": "Push not found or already stopped" }
Store pushId when you start the push. It is the only handle for stopping it. A push whose id belongs to another project, or that has already stopped, returns 404.
Stop your pushes
A push keeps running and keeps counting against your usage until you stop it or the channel closes. Stop it in the same code path that ends the stream, and again from a scheduled sweep if your stream can end without a clean shutdown.
What the push looks like
- The output is a grid composite of everyone publishing, not a single participant's feed.
- Up to three destinations per push. If you need more, start a second push for the same channel.
- Only
rtmp://andrtmps://destinations are accepted; anything else is rejected with 400.
AppLooma does not store your media
The push sends video to destinations you nominate. AppLooma does not keep a copy of the stream. If you want an archive, record it on the destination platform, which usually offers this on the broadcast itself.