# Metered Video SDK — llms.txt reference
Metered Video is a real-time video and audio calling SDK. It lets you build group video/audio
calling apps, live streaming, recording, composition, and RTMP-out to third-party services.
## Getting Started
1. **Sign up** at https://dashboard.metered.ca/signup to create a free Metered account.
2. After signup your app is created automatically. Your **app name** (e.g. `yourappname`) forms your domain: `yourappname.metered.live`. Find it on the Dashboard home page.
3. Go to **Dashboard → Developers** to find your **Secret Key**. This is used for all server-side REST API calls. Never expose it in front-end code.
4. Rooms are created via the Dashboard or the REST API. Each room has a URL: `yourappname.metered.live/roomName`.
CDN import (JavaScript):
```
```
REST API base URL:
```
https://.metered.live/api/v1
```
Authentication: pass `secretKey` as a query parameter on every REST call (e.g. `?secretKey=YOUR_SECRET_KEY`).
Key concepts:
- Room: a meeting space with a unique URL (`.metered.live/`).
- Session (meetingSession): one call that happened inside a room.
- Participant Session: one user's participation in a session.
---
## Setup
```javascript
// 1. Include SDK
//
// 2. Create meeting object
const meeting = new Metered.Meeting();
// 3. Join a room
const meetingInfo = await meeting.join({
roomURL: "yourapp.metered.live/your-room-name",
name: "John Doe"
});
// 4. Share camera / mic
await meeting.startVideo();
await meeting.startAudio();
// 5. Handle remote streams
meeting.on("remoteTrackStarted", function(trackItem) {
var stream = new MediaStream([trackItem.track]);
var video = document.createElement("video");
video.id = trackItem.streamId;
video.autoplay = true;
video.srcObject = stream;
document.getElementById("container").append(video);
});
meeting.on("remoteTrackStopped", function(trackItem) {
document.getElementById(trackItem.streamId).remove();
});
// 6. Leave
await meeting.leaveMeeting();
```
---
## SDK Methods
All methods are called on the `meeting` object (`new Metered.Meeting()`).
### join(options)
Join a meeting room. Must be called before any media methods.
```javascript
const meetingInfo = await meeting.join(options);
```
| Parameter | Type | Required | Description |
|---|---|---|---|
| roomURL | string | yes | `.metered.live/` |
| name | string | yes | Display name for the participant |
| accessToken | string | no | Token from `/api/v1/token`. Required for private rooms |
| receiveVideoStreamType | string | no | `only_individual` (default), `none`, `only_composed`, `all` |
| receiveAudioStreamType | string | no | `only_individual` (default), `none`, `only_composed`, `all` |
Returns `meetingInfo`:
```
{
roomId: string,
meetingSessionId: string,
participantSessionId: string,
onlineParticipants: [{ _id, name, isAdmin, meetingSessionId, roomId }]
}
```
### leaveMeeting()
Leave the meeting. Emits `stateChanged`, `meetingLeft` to self and `participantLeft` to others.
```javascript
await meeting.leaveMeeting();
```
Returns: void
### startVideo()
Share the participant's camera. Browser will prompt for camera permission. **Must call `join()` first.**
```javascript
await meeting.startVideo();
```
Returns: void. Emits `localTrackStarted` (type "video").
### stopVideo()
Stop sharing camera or screen. Emits `localTrackStopped` to self, `remoteTrackStopped` to others.
```javascript
await meeting.stopVideo();
```
Returns: void
### startAudio()
Share the participant's microphone. Browser will prompt for mic permission. **Must call `join()` first.**
```javascript
await meeting.startAudio();
```
Returns: void. Emits `localTrackStarted` (type "audio").
### stopAudio()
Stop sharing microphone. Emits `localTrackStopped` to self, `remoteTrackStopped` to others.
```javascript
await meeting.stopAudio();
```
Returns: void
### startScreenShare()
Share the participant's screen. Shows browser popup to select screen/window/tab. Replaces camera feed if one is active. **Must call `join()` first.**
```javascript
await meeting.startScreenShare();
```
Returns: void. Emits `localTrackStarted` (type "video") if no camera was active, or `localTrackUpdated` if replacing an existing camera feed.
### muteLocalAudio()
Mute microphone without stopping the audio stream. `remoteTrackStopped` is NOT emitted. Browser still shows mic as in use.
```javascript
await meeting.muteLocalAudio();
```
Returns: void
### unmuteLocalAudio()
Unmute previously muted microphone. `remoteTrackStarted` is NOT emitted.
```javascript
meeting.unmuteLocalAudio();
```
Returns: void
### pauseLocalVideo()
Mute video (black frame sent) without stopping the video stream. `remoteTrackStopped` is NOT emitted.
```javascript
await meeting.pauseLocalVideo();
```
Returns: void
### resumeLocalVideo()
Resume previously paused video. `remoteTrackStarted` is NOT emitted.
```javascript
await meeting.resumeLocalVideo();
```
Returns: void
### listVideoInputDevices()
List all cameras connected to the device. Browser will prompt for camera permission.
```javascript
const devices = await meeting.listVideoInputDevices();
// => [{ deviceId: string, groupId: string, label: string }]
```
Returns: Array of `{ deviceId, groupId, label }`
### listAudioInputDevices()
List all microphones connected to the device. Browser will prompt for mic permission.
```javascript
const devices = await meeting.listAudioInputDevices();
// => [{ deviceId: string, groupId: string, label: string }]
```
Returns: Array of `{ deviceId, groupId, label }`
### listAudioOutputDevices()
List all speakers/audio-output devices. Not supported in Firefox.
```javascript
const devices = await meeting.listAudioOutputDevices();
// => [{ deviceId: string, groupId: string, label: string }]
```
Returns: Array of `{ deviceId, groupId, label }`
### chooseVideoInputDevice(deviceId)
Select or switch camera. Can be called before or after join().
```javascript
await meeting.chooseVideoInputDevice(deviceId);
```
| Parameter | Type | Required | Description |
|---|---|---|---|
| deviceId | string | yes | From `listVideoInputDevices()` |
Returns: void
### chooseAudioInputDevice(deviceId)
Select or switch microphone. Can be called before or after join().
```javascript
await meeting.chooseAudioInputDevice(deviceId);
```
| Parameter | Type | Required | Description |
|---|---|---|---|
| deviceId | string | yes | From `listAudioInputDevices()` |
Returns: void
### chooseAudioOutputDevice(deviceId)
Select or switch speaker. Not supported in Firefox.
```javascript
await meeting.chooseAudioOutputDevice(deviceId);
```
| Parameter | Type | Required | Description |
|---|---|---|---|
| deviceId | string | yes | From `listAudioOutputDevices()` |
Returns: void
### getOnlineParticipants()
Return the list of participants currently in the meeting. Synchronous call.
```javascript
const participants = meeting.getOnlineParticipants();
```
Returns: Array of `{ _id, name, isAdmin: boolean, meetingSessionId, roomId }`
### getLocalVideoStream()
Get the MediaStream of the selected (or default) video input device.
```javascript
var stream = await meeting.getLocalVideoStream();
```
Returns: MediaStream. Throws if no video device available.
### shareCustomVideoTrack(videoTrack)
Share a custom video MediaStreamTrack (e.g., from a canvas or processed stream) instead of the camera. join() must be called first.
```javascript
await meeting.shareCustomVideoTrack(videoTrack);
```
| Parameter | Type | Required | Description |
|---|---|---|---|
| videoTrack | MediaStreamTrack | yes | Video track to share |
Returns: MediaStreamTrack. Emits `localTrackStarted` (first time) or `localTrackUpdated` (replacing existing).
### shareCustomAudio(audioStream)
Share a custom audio MediaStream (e.g., from Web Audio API) instead of the microphone. join() must be called first.
```javascript
await meeting.shareCustomAudio(audioStream);
```
| Parameter | Type | Required | Description |
|---|---|---|---|
| audioStream | MediaStream | yes | Stream containing an audio track |
Returns: void. Emits `localTrackStarted` when no prior audio producer exists. Unlike video, replacing an existing audio track does NOT emit `localTrackUpdated`.
---
## SDK Events
All events are listened to via `meeting.on(eventName, callback)`.
### localTrackStarted
Fired when `startAudio()`, `startVideo()`, `startScreenShare()`, `shareCustomVideoTrack()`, or `shareCustomAudio()` is called.
```javascript
meeting.on("localTrackStarted", function(localTrackItem) { });
```
Callback data (`localTrackItem`):
| Field | Type | Description |
|---|---|---|
| type | string | `"audio"` or `"video"` |
| streamId | string | Unique stream identifier |
| track | MediaStreamTrack | The media track |
### localTrackStopped
Fired when `stopAudio()`, `stopVideo()` is called, or when custom tracks end.
```javascript
meeting.on("localTrackStopped", function(localTrackItem) { });
```
Callback data: same shape as `localTrackStarted`.
### localTrackUpdated
Fired when an existing local track is replaced (e.g., `shareCustomVideoTrack()` while already sharing, switching devices, switching between camera and screen share).
```javascript
meeting.on("localTrackUpdated", function(localTrackItem) { });
```
Callback data: same shape as `localTrackStarted`.
### remoteTrackStarted
Fired when any remote participant shares their mic, camera, or screen.
```javascript
meeting.on("remoteTrackStarted", function(remoteTrackItem) { });
```
Callback data (`remoteTrackItem`):
| Field | Type | Description |
|---|---|---|
| streamId | string | Unique stream identifier |
| type | string | `"audio"` or `"video"` |
| participantSessionId | string | Source participant's session ID |
| track | MediaStreamTrack | The media track (wrap in `new MediaStream([track])` for HTML video) |
| name | string | Source participant's display name |
| isComposedStream | boolean | `true` if this is a composed stream |
### remoteTrackStopped
Fired when a remote participant stops sharing mic, camera, or screen.
```javascript
meeting.on("remoteTrackStopped", function(remoteTrackItem) { });
```
Callback data: same shape as `remoteTrackStarted`.
### participantJoined
Fired to all existing participants when a new participant joins.
```javascript
meeting.on("participantJoined", function(participantInfo) { });
```
Callback data (`participantInfo`):
| Field | Type | Description |
|---|---|---|
| _id | string | participantSessionId |
| name | string | Display name |
| isAdmin | boolean | Whether participant is admin |
| meetingSessionId | string | Current meeting session ID |
| roomId | string | Room ID |
| email | string | Email if specified |
| externalUserId | string | External user ID if specified |
| meta | string | Custom metadata if specified |
### participantLeft
Fired to all other participants when someone leaves the meeting.
```javascript
meeting.on("participantLeft", function(participantInfo) { });
```
Callback data: same shape as `participantJoined`.
### onlineParticipants
Fired periodically during the meeting with the current list of online participants.
```javascript
meeting.on("onlineParticipants", function(onlineParticipants) { });
```
Callback data: Array of `{ _id, name, isAdmin, meetingSessionId, roomId }`
### stateChanged
Fired when the meeting connection state changes.
```javascript
meeting.on("stateChanged", function(meetingState) { });
```
`meetingState` values:
- `"not_joined"` - initial or error during join
- `"joining"` - join() called
- `"connecting_streams"` - connected to server, receiving streams
- `"joined"` - fully connected
- `"reconnect_success"` - reconnected after interruption
- `"network_connection_lost"` - connection interrupted
- `"network_connection_restored"` - connection restored
- `"terminated"` - meeting ended
### meetingEnded
Fired when the meeting is terminated (duration limit reached, admin ejected participant, room expired with ejectOnExpiry).
```javascript
meeting.on("meetingEnded", function() { });
```
Callback data: none.
### meetingLeft
Fired to the local participant after calling `leaveMeeting()`. Not emitted to other participants.
```javascript
meeting.on("meetingLeft", function() { });
```
Callback data: none.
### activeSpeaker
Fired when a participant is speaking.
```javascript
meeting.on("activeSpeaker", function(speakerInfo) { });
```
Callback data (`speakerInfo`):
| Field | Type | Description |
|---|---|---|
| streamId | string | Stream ID |
| meetingSessionId | string | Meeting session ID |
| name | string | Speaker's display name |
| roomId | string | Room ID |
| participantSessionId | string | Speaker's participant session ID |
| volumeLevel | number | -128 (silent) to 0 (loudest) |
### composedTrackStarted
Fired when a composed media stream is received (requires composition enabled on the room).
```javascript
meeting.on("composedTrackStarted", function(remoteTrackItem) { });
```
Callback data: same shape as `remoteTrackStarted` with `isComposedStream: true`.
---
## REST API
Base URL: `https://.metered.live/api/v1`
Auth: `?secretKey=` on every request.
Content-Type for POST/PUT bodies: `application/json`
---
### Room Endpoints
#### POST /room -- Create Room
Create a new meeting room.
```
POST https://.metered.live/api/v1/room?secretKey=
Content-Type: application/json
```
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
| roomName | string | no | URL-friendly name; auto-generated if omitted |
| privacy | string | yes | `"public"` or `"private"` |
| expireUnixSec | integer | no | Room expiry (unix seconds) |
| ejectAtRoomExp | boolean | no | Eject participants at expiry |
| notBeforeUnixSec | integer | no | Earliest join time (unix seconds) |
| maxParticipants | integer | no | Max participants allowed |
| autoJoin | boolean | no | Auto join |
| enableRequestToJoin | boolean | no | Allow request-to-join for private rooms |
| enableChat | boolean | no | Enable chat (iframe only) |
| enableScreenSharing | boolean | no | Enable screen sharing (iframe only) |
| joinVideoOn | boolean | no | Camera on by default (iframe only) |
| joinAudioOn | boolean | no | Mic on by default (iframe only) |
| ownerOnlyBroadcast | boolean | no | Only admin can share media |
| enableRecording | boolean | no | Allow recording |
| recordRoom | boolean | no | Auto-record the meeting |
| ejectAfterElapsedTimeInSec | integer | no | Max participant duration (seconds) |
| meetingJoinWebhook | string | no | Webhook URL for join events |
| meetingLeftWebhook | string | no | Webhook URL for leave events |
| meetingStartedWebhook | string | no | Webhook URL for session start |
| meetingEndedWebhook | string | no | Webhook URL for session end |
| endMeetingAfterNoActivityInSec | integer | no | Auto-end after inactivity (seconds) |
| audioOnlyRoom | boolean | no | Audio-only room (audio pricing) |
| enableComposition | boolean | no | Enable composition (required for live streaming, recording composed, RTMP out) |
| compositionLayout | string | no | `"grid"` or `"active_speaker"` |
| recordComposition | boolean | no | Record the composed stream |
| enableRTMPOut | boolean | no | Stream to third-party RTMP services |
| rtmpOutURL | string | conditional | RTMP ingest URL (required when enableRTMPOut is true) |
| enableLiveStreaming | boolean | no | Enable HLS live streaming |
| showInviteBox | boolean | no | Show invite box (iframe only) |
| newChatForMeetingSession | boolean | no | New chat history per meeting session (iframe only) |
| deleteOnExp | boolean | no | Delete room when it expires |
Response 200:
```json
{
"_id": "string",
"roomName": "string",
"privacy": "public",
"app": "string",
"created": "ISO date",
"deleteOnExp": false,
"autoJoin": false,
"enableRequestToJoin": false,
"enableChat": false,
"enableScreenSharing": true,
"joinVideoOn": true,
"joinAudioOn": true,
"ownerOnlyBroadcast": false,
"recordRoom": false,
"ejectAtRoomExp": false,
"archived": false,
"lang": "en"
}
```
#### GET /room/{roomName} -- Get Room
```
GET https://.metered.live/api/v1/room/{roomName}?secretKey=
```
Response 200: Room object (same shape as create response).
#### GET /rooms -- Get All Rooms
```
GET https://.metered.live/api/v1/rooms?secretKey=
```
Response 200: Paginated list.
```json
{
"pages": 4,
"limit": 10,
"skip": 0,
"data": [ { ...room with participantSessions... } ]
}
```
#### PUT /room/{roomName} -- Update Room
```
PUT https://.metered.live/api/v1/room/{roomName}?secretKey=
Content-Type: application/json
```
Request body: same fields as Create Room. All are optional except `privacy`.
Response 200: Updated room object.
#### DELETE /room/{roomName} -- Delete Room
```
DELETE https://.metered.live/api/v1/room/{roomName}?secretKey=
```
Response 200:
```json
{ "success": true, "message": "Room successfully archived" }
```
---
### Token Endpoints
#### POST /token -- Generate Access Token
Generate a token for joining private rooms or assigning identity.
```
POST https://.metered.live/api/v1/token?secretKey=
Content-Type: application/json
```
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
| isAdmin | boolean | no | Grant admin privileges |
| roomName | string | no | Restrict token to specific room |
| globalToken | boolean | no | Token valid for all rooms |
| name | string | no | Participant display name |
| email | string | no | Participant email |
| meta | string | no | Custom metadata |
| externalUserId | string | no | Your system's user ID |
| expireUnixSec | integer | no | Token expiry (unix seconds) |
| notBeforeUnixSec | integer | no | Token not valid before (unix seconds) |
| ejectAfterElapsedTimeInSec | integer | no | Auto-eject after N seconds |
| joinVideoOn | boolean | no | Camera on by default (iframe) |
| joinAudioOn | boolean | no | Mic on by default (iframe) |
| disableVideo | boolean | no | Disable video (iframe) |
| disableAudio | boolean | no | Disable audio (iframe) |
| disableScreenSharing | boolean | no | Disable screen sharing (iframe) |
Response 200:
```json
{ "token": "" }
```
#### POST /token/validate -- Validate Token
**Note:** This endpoint does NOT require the `secretKey` query parameter.
```
POST https://.metered.live/api/v1/token/validate
Content-Type: application/json
```
Request body:
| Field | Type | Required |
|---|---|---|
| token | string | yes |
Response 200:
```json
{ "name": "James Bond" }
```
---
### Meeting Session Endpoints
#### GET /rooms/active/meetingsessions -- Get Active Sessions
```
GET https://.metered.live/api/v1/rooms/active/meetingsessions?secretKey=
```
Response 200: Array of active meeting sessions with embedded room objects.
```json
[{
"_id": "string",
"app": "string",
"room": { ...room object... },
"startTime": 1627654459,
"state": "active"
}]
```
#### GET /room/{roomName}/meetingsessions -- Get All Sessions for Room
```
GET https://.metered.live/api/v1/room/{roomName}/meetingsessions?secretKey=
```
Response 200: Paginated.
```json
{
"pages": 4,
"limit": 10,
"skip": 0,
"data": [{
"_id": "string",
"app": "string",
"room": "string",
"startTime": 1627598254,
"state": "ended",
"endTime": 1627598896,
"participantSessions": [{
"_id": "string",
"name": "string",
"joinTime": 1627598934,
"leftTime": 1627599054,
"state": "exited",
"browser": "Chrome",
"os": "Windows",
"isAdmin": false,
"authenticatedUsingToken": true,
"externalUserId": "string",
"email": "string"
}]
}]
}
```
#### GET /room/{roomName}/presence -- Get Meeting Presence
Get the current live presence for a room (active sessions with participants).
```
GET https://.metered.live/api/v1/room/{roomName}/presence?secretKey=
```
Response 200: Array of active sessions with room details (same shape as active sessions).
---
### Recording Endpoints
#### GET /recordings -- Get All Recordings
```
GET https://.metered.live/api/v1/recordings?secretKey=
```
Response 200:
```json
{
"currEndId": "string",
"total": 139,
"limit": 50,
"data": [{
"_id": "string",
"app": "string",
"room": "string",
"participantSession": "string",
"meetingSession": "string",
"participantUsername": "string",
"fileName": "uuid.webm",
"sizeInBytes": 4526446,
"type": "video",
"startTime": 1636847920,
"endTime": 1636847953
}]
}
```
#### GET /recordings/room/{roomName} -- Get Recordings for a Room
```
GET https://.metered.live/api/v1/recordings/room/{roomName}?secretKey=
```
Response 200: Same paginated recording structure.
#### GET /recordings/meetingsession/{meetingSessionId} -- Get Recordings for a Meeting Session
```
GET https://.metered.live/api/v1/recordings/meetingsession/{meetingSessionId}?secretKey=
```
Response 200: Same paginated recording structure.
#### GET /recordings/participantsession/{participantSessionId} -- Get Recordings for a Participant Session
```
GET https://.metered.live/api/v1/recordings/participantsession/{participantSessionId}?secretKey=
```
Response 200: Same paginated recording structure.
#### GET /recording/{recordingId}/download -- Download Recording
Returns a pre-signed S3 URL for downloading.
```
GET https://.metered.live/api/v1/recording/{recordingId}/download?secretKey=
```
Response 200:
```json
{ "url": "https://metered-recording.s3...presigned-url" }
```
#### DELETE /recording/{recordingId} -- Delete Recording
```
DELETE https://.metered.live/api/v1/recording/{recordingId}?secretKey=
```
Response 200:
```json
{ "success": true, "recordingId": "string", "message": "recording deleted successfully" }
```
---
### Live Streaming (Real-Time) Endpoints
These endpoints manage dedicated real-time live streaming rooms (separate from meeting rooms).
#### POST /realtimelivestreaming/room -- Create Live Streaming Room
```
POST https://.metered.live/api/v1/realtimelivestreaming/room?secretKey=
Content-Type: application/json
```
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
| roomName | string | no | Auto-generated if omitted |
| enableLiveStreaming | boolean | no | Enable HLS streaming |
| displayMessage | string | no | Message when stream is offline |
| customCSS | string | no | Custom CSS for viewer |
| enableRecording | boolean | no | Record the livestream |
| recordComposition | boolean | no | Record composed audio+video |
| enableRTMPOut | boolean | no | Enable RTMP out |
| rtmpOutURL | string | no | Third-party RTMP ingest URL |
Response 200:
```json
{
"_id": "string",
"roomName": "mystreamingroom",
"viewerURL": "https://.metered.live//player",
"broadcastManager": "https://dashboard.metered.ca/livestreaming-rooms//app//broadcast",
"currentlyBroadcasting": false,
"created": "ISO date"
}
```
#### PUT /realtimelivestreaming/room/{roomName} -- Update Live Streaming Room
```
PUT https://.metered.live/api/v1/realtimelivestreaming/room/{roomName}?secretKey=
Content-Type: application/json
```
Request body: same fields as create. Response: same shape.
#### GET /realtimelivestreaming/rooms -- Get All Live Streaming Rooms
```
GET https://.metered.live/api/v1/realtimelivestreaming/rooms?secretKey=
```
Response 200: Array of live streaming room objects.
#### GET /realtimelivestreaming/room/{roomName} -- Get Live Streaming Room
```
GET https://.metered.live/api/v1/realtimelivestreaming/room/{roomName}?secretKey=
```
Response 200: Single live streaming room object.
#### GET /realtimelivestreaming/rooms/broadcasting -- Currently Broadcasting Rooms
```
GET https://.metered.live/api/v1/realtimelivestreaming/rooms/broadcasting?secretKey=
```
Response 200: Array of rooms with `currentlyBroadcasting: true`.
---
## Recording
Two recording modes:
1. Individual recording: records each participant's audio and video as separate files. Enable with `recordRoom: true` when creating/updating a room.
2. Composed recording: records all participants combined into a single video/audio file. Enable with `recordComposition: true` and `enableComposition: true` on the room.
Enable recording via Dashboard (Room settings > Record Room toggle) or via REST API (POST/PUT /room).
Manage recordings via:
- Dashboard: Cloud Recording section for viewing, downloading, previewing, deleting.
- REST API: GET /recordings, GET /recordings/room/{roomName}, GET /recording/{id}/download, DELETE /recording/{id}.
Recording object shape:
```json
{
"_id": "string",
"app": "string",
"room": "string",
"participantSession": "string",
"meetingSession": "string",
"participantUsername": "string",
"fileName": "uuid.webm",
"sizeInBytes": 4526446,
"type": "video",
"startTime": 1636847920,
"endTime": 1636847953
}
```
---
## Live Streaming
Live streaming sends an HLS stream of the meeting that can be viewed by millions of users.
Requirements:
- Composition must be enabled (it is auto-enabled when live streaming is turned on).
- Enable via Dashboard (Room settings > HLS/Dash Live Streaming toggle) or via API (`enableLiveStreaming: true`).
Composition layouts: `grid`, `active_speaker`.
HLS playback URL format:
```
https://live.metered.ca/hls/{PLAYBACK_ID}.m3u8
```
The HLS URL is visible in Dashboard under Room summary once live streaming is enabled.
Playing the stream in a browser (using hls.js):
```html
```
iOS (AVPlayer) and Android (ExoPlayer) support HLS natively.
Receiving composed streams in the SDK: set `receiveVideoStreamType: "only_composed"` or `"all"` in `join()` options, then listen for the `composedTrackStarted` event.
---
## Composition
Composition combines multiple participant audio/video streams into a single composed stream.
Use cases:
- Live streaming (required)
- Composed recording (single video file of the whole meeting)
- RTMP OUT to third-party services
- Low-bandwidth participants (receive one stream instead of N)
Enable via:
- Dashboard: Room settings > Composition toggle
- API: `enableComposition: true` on create/update room
Layouts: `grid` (default), `active_speaker`
Set layout via API: `compositionLayout: "grid"` or `"active_speaker"`
SDK integration: when joining a room with composition enabled, use `receiveVideoStreamType` / `receiveAudioStreamType` set to `"only_composed"` or `"all"` in the `join()` options. The composed stream arrives via the `composedTrackStarted` event (or `remoteTrackStarted` with `isComposedStream: true`).
---
## RTMP OUT
RTMP OUT sends the composed meeting stream to third-party RTMP services (YouTube Live, Facebook Live, Twitch, etc.).
Requirements:
- Composition is auto-enabled when RTMP OUT is turned on.
- An RTMP ingest URL from the third-party service.
Enable via:
- Dashboard: Room settings > Enable RTMP OUT, then provide the RTMP URL.
- API: set `enableRTMPOut: true` and `rtmpOutURL: "rtmp://..."` when creating/updating a room.
The meeting will be streamed to the RTMP URL whenever participants are in the room with composition active.
---
## Webhooks
Rooms support four webhook URLs set via create/update room API:
| Field | Trigger |
|---|---|
| meetingJoinWebhook | Participant joins the room |
| meetingLeftWebhook | Participant leaves the room |
| meetingStartedWebhook | A new meeting session starts |
| meetingEndedWebhook | A meeting session ends |
---
## Error Responses
All REST endpoints return errors in this format:
```json
{ "success": false, "message": "error description" }
```
Common HTTP status codes: 200 (success), 400 (bad request), 500 (server error).
---
## Complete Quick-Start Example (HTML)
This is a minimal but complete working example that creates a two-person video call:
```html
Metered Quick Start
Metered Video Demo
Local Video
Remote Videos
```
---
## Device Selection Pattern
List available devices and let the user choose before or during a call:
```javascript
// List cameras
const cameras = await meeting.listVideoInputDevices();
// cameras = [{ deviceId: "abc123", groupId: "grp1", label: "Built-in Camera" }, ...]
// List microphones
const mics = await meeting.listAudioInputDevices();
// List speakers (not supported in Firefox)
const speakers = await meeting.listAudioOutputDevices();
// Select a specific camera (can be called before or after join)
await meeting.chooseVideoInputDevice(cameras[1].deviceId);
// Select a specific microphone
await meeting.chooseAudioInputDevice(mics[0].deviceId);
// Select a specific speaker
await meeting.chooseAudioOutputDevice(speakers[0].deviceId);
// After selecting, start sharing
await meeting.startVideo();
await meeting.startAudio();
```
Note: In Firefox, the `label` property may be blank if the user has not checked
"Remember this decision" when granting permissions.
---
## Custom Video Track Pattern (Canvas, Processed Video)
Share a canvas element or processed video stream instead of a camera:
```javascript
// Example: Share a canvas as video
const canvas = document.getElementById('myCanvas');
const canvasStream = canvas.captureStream(30); // 30 FPS
const videoTrack = canvasStream.getVideoTracks()[0];
await meeting.shareCustomVideoTrack(videoTrack);
// Example: Apply a filter to camera using canvas
const localStream = await meeting.getLocalVideoStream();
const videoEl = document.createElement('video');
videoEl.srcObject = localStream;
await videoEl.play();
const canvas2 = document.createElement('canvas');
const ctx = canvas2.getContext('2d');
function drawFrame() {
ctx.drawImage(videoEl, 0, 0);
// Apply any canvas manipulations here (filters, overlays, etc.)
ctx.filter = 'grayscale(100%)';
ctx.drawImage(videoEl, 0, 0);
requestAnimationFrame(drawFrame);
}
drawFrame();
const processedStream = canvas2.captureStream(30);
const processedTrack = processedStream.getVideoTracks()[0];
await meeting.shareCustomVideoTrack(processedTrack);
```
---
## Custom Audio Pattern (Web Audio API)
Share processed audio or synthesized audio instead of a microphone:
```javascript
const audioContext = new AudioContext();
// Example: Mix microphone with background audio
const micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
const micSource = audioContext.createMediaStreamSource(micStream);
const destination = audioContext.createMediaStreamDestination();
// Add gain control
const gainNode = audioContext.createGain();
gainNode.gain.value = 0.8;
micSource.connect(gainNode);
gainNode.connect(destination);
// Share the processed audio
await meeting.shareCustomAudio(destination.stream);
```
---
## Private Rooms and Authentication Flow
1. Create a private room:
```
POST /api/v1/room?secretKey=
{ "roomName": "secure-room", "privacy": "private" }
```
2. Generate an access token for each user (server-side):
```
POST /api/v1/token?secretKey=
{
"roomName": "secure-room",
"name": "Alice",
"isAdmin": false,
"email": "alice@example.com",
"externalUserId": "user-123",
"ejectAfterElapsedTimeInSec": 3600
}
```
Response: `{ "token": "" }`
3. Client joins with the token:
```javascript
const meetingInfo = await meeting.join({
roomURL: "yourapp.metered.live/secure-room",
name: "Alice",
accessToken: ""
});
```
4. Validate a token (optional, server-side):
```
POST /api/v1/token/validate
{ "token": "" }
```
Response: `{ "name": "Alice" }`
Admin users can:
- Allow other users without tokens to join private meetings (request-to-join flow)
- Be the only ones who can broadcast (if `ownerOnlyBroadcast` is enabled on the room)
---
## REST API cURL Examples
Create a room:
```bash
curl -X POST \
'https://yourapp.metered.live/api/v1/room?secretKey=YOUR_SECRET_KEY' \
-H 'Content-Type: application/json' \
-d '{
"roomName": "my-meeting",
"privacy": "public",
"enableRecording": true,
"ejectAfterElapsedTimeInSec": 7200
}'
```
Get a room:
```bash
curl -X GET \
'https://yourapp.metered.live/api/v1/room/my-meeting?secretKey=YOUR_SECRET_KEY' \
-H 'Accept: application/json'
```
Update a room:
```bash
curl -X PUT \
'https://yourapp.metered.live/api/v1/room/my-meeting?secretKey=YOUR_SECRET_KEY' \
-H 'Content-Type: application/json' \
-d '{
"enableComposition": true,
"compositionLayout": "active_speaker",
"enableLiveStreaming": true,
"enableRTMPOut": true,
"rtmpOutURL": "rtmp://a.rtmp.youtube.com/live2/your-stream-key"
}'
```
Delete a room:
```bash
curl -X DELETE \
'https://yourapp.metered.live/api/v1/room/my-meeting?secretKey=YOUR_SECRET_KEY' \
-H 'Accept: application/json'
```
Generate access token:
```bash
curl -X POST \
'https://yourapp.metered.live/api/v1/token?secretKey=YOUR_SECRET_KEY' \
-H 'Content-Type: application/json' \
-d '{
"roomName": "my-meeting",
"name": "John Doe",
"isAdmin": true,
"externalUserId": "user-456",
"ejectAfterElapsedTimeInSec": 1800
}'
```
Get all recordings:
```bash
curl -X GET \
'https://yourapp.metered.live/api/v1/recordings?secretKey=YOUR_SECRET_KEY' \
-H 'Accept: application/json'
```
Download a recording:
```bash
curl -X GET \
'https://yourapp.metered.live/api/v1/recording/RECORDING_ID/download?secretKey=YOUR_SECRET_KEY' \
-H 'Accept: application/json'
```
Get active meeting sessions:
```bash
curl -X GET \
'https://yourapp.metered.live/api/v1/rooms/active/meetingsessions?secretKey=YOUR_SECRET_KEY' \
-H 'Accept: application/json'
```
Create a live streaming room:
```bash
curl -X POST \
'https://yourapp.metered.live/api/v1/realtimelivestreaming/room?secretKey=YOUR_SECRET_KEY' \
-H 'Content-Type: application/json' \
-d '{ "roomName": "my-livestream" }'
```
---
## Room Configuration Reference
These fields control room behavior and can be set via POST /room or PUT /room/{roomName}:
Core settings:
- `roomName` (string): URL-friendly room name; auto-generated if omitted.
- `privacy` ("public"|"private"): Public rooms are open; private rooms require an access token.
- `maxParticipants` (integer): Cap on concurrent participants.
- `expireUnixSec` (integer): Room expiry time in unix seconds.
- `notBeforeUnixSec` (integer): Earliest time users can join.
- `ejectAtRoomExp` (boolean): Kick everyone when room expires.
- `ejectAfterElapsedTimeInSec` (integer): Max time per participant in seconds.
- `endMeetingAfterNoActivityInSec` (integer): Auto-end if no one is sharing media.
- `audioOnlyRoom` (boolean): Restrict to audio-only (different pricing tier).
Media defaults (iframe only):
- `joinVideoOn` (boolean): Camera on when joining.
- `joinAudioOn` (boolean): Mic on when joining.
- `enableScreenSharing` (boolean): Allow screen sharing.
- `enableChat` (boolean): Enable text chat.
- `ownerOnlyBroadcast` (boolean): Only admins can share media.
- `autoJoin` (boolean): Auto-join without lobby.
- `enableRequestToJoin` (boolean): Allow non-token users to request joining private rooms.
- `newChatForMeetingSession` (boolean): Fresh chat per session.
- `showInviteBox` (boolean): Show invite UI when room is empty.
Recording:
- `enableRecording` (boolean): Allow manual recording.
- `recordRoom` (boolean): Auto-record every session.
- `recordComposition` (boolean): Record the composed (combined) stream.
Composition and streaming:
- `enableComposition` (boolean): Combine all streams into one. Required for live streaming and RTMP out.
- `compositionLayout` ("grid"|"active_speaker"): Layout for composed stream.
- `enableLiveStreaming` (boolean): Generate HLS URL for the meeting.
- `enableRTMPOut` (boolean): Push composed stream to RTMP endpoint.
- `rtmpOutURL` (string): RTMP ingest URL (e.g., YouTube Live, Twitch).
Webhooks:
- `meetingJoinWebhook` (string): URL called when a participant joins.
- `meetingLeftWebhook` (string): URL called when a participant leaves.
- `meetingStartedWebhook` (string): URL called when a meeting session starts.
- `meetingEndedWebhook` (string): URL called when a meeting session ends.
---
## Meeting State Lifecycle
```
not_joined -> joining -> connecting_streams -> joined
|
v
network_connection_lost
|
v
network_connection_restored
|
v
reconnect_success -> joined
joined -> terminated (when meetingEnded fires)
joined -> not_joined (when leaveMeeting() called)
```
States:
- `not_joined`: Initial state or failed to join.
- `joining`: join() has been called, connecting to server.
- `connecting_streams`: Connected, receiving initial media streams.
- `joined`: Fully connected and operational.
- `network_connection_lost`: Network interruption detected.
- `network_connection_restored`: Network back, reconnecting.
- `reconnect_success`: Successfully reconnected.
- `terminated`: Meeting ended externally (admin eject, room expiry, duration limit).
---
## Live Streaming Playback Reference
HLS URL format: `https://live.metered.ca/hls/{PLAYBACK_ID}.m3u8`
The PLAYBACK_ID / HLS URL is available from the room details in the Metered dashboard
after enabling live streaming.
Browser playback with hls.js:
```html
```
React playback:
```jsx
import React, { useEffect, useRef } from "react";
import Hls from "hls.js";
export default function LivePlayer() {
const videoRef = useRef(null);
const src = "https://live.metered.ca/hls/PLAYBACK_ID.m3u8";
useEffect(() => {
let hls;
if (videoRef.current) {
const video = videoRef.current;
if (video.canPlayType("application/vnd.apple.mpegurl")) {
video.src = src;
} else if (Hls.isSupported()) {
hls = new Hls();
hls.loadSource(src);
hls.attachMedia(video);
}
}
return () => { if (hls) hls.destroy(); };
}, [videoRef]);
return ;
}
```
Android (ExoPlayer):
```java
SimpleExoPlayer player = new SimpleExoPlayer.Builder(context).build();
player.setMediaItem(MediaItem.fromUri("https://live.metered.ca/hls/PLAYBACK_ID.m3u8"));
player.prepare();
```
iOS/tvOS (AVPlayer):
```swift
import AVKit
let player = AVPlayer(url: URL(string: "https://live.metered.ca/hls/PLAYBACK_ID.m3u8")!)
// Use with AVPlayerViewController or AVPlayerLayer
player.play()
```
Recommended players for web: hls.js (free), Plyr.io (free), JWPlayer, Brightcove, THEOplayer, Bitmovin.
---
## RTMP OUT Setup Examples
YouTube Live:
1. Go to YouTube Studio > Go Live > Stream
2. Copy the Stream URL and Stream Key
3. Combine them: `rtmp://a.rtmp.youtube.com/live2/YOUR_STREAM_KEY`
4. Set on room: `enableRTMPOut: true, rtmpOutURL: "rtmp://a.rtmp.youtube.com/live2/YOUR_STREAM_KEY"`
Facebook Live:
1. Go to Facebook Live Producer
2. Copy the Server URL and Stream Key
3. Set on room: `rtmpOutURL: "rtmps://live-api-s.facebook.com:443/rtmp/YOUR_STREAM_KEY"`
Twitch:
1. Go to Twitch Dashboard > Settings > Stream
2. Copy the Primary Stream Key
3. Set on room: `rtmpOutURL: "rtmp://live.twitch.tv/app/YOUR_STREAM_KEY"`
Other services: use the RTMP ingest URL provided by the service.
---
## Composed Stream Receiver Pattern
To receive and display only the composed (combined) stream instead of individual streams:
```javascript
const meeting = new Metered.Meeting();
await meeting.join({
roomURL: "yourapp.metered.live/composed-room",
name: "Viewer",
receiveVideoStreamType: "only_composed",
receiveAudioStreamType: "only_composed"
});
// The composed stream arrives via composedTrackStarted
meeting.on("composedTrackStarted", function(remoteTrackItem) {
var track = remoteTrackItem.track;
var stream = new MediaStream([track]);
if (remoteTrackItem.type === "video") {
document.getElementById("composedVideo").srcObject = stream;
}
if (remoteTrackItem.type === "audio") {
var audio = document.createElement("audio");
audio.autoplay = true;
audio.srcObject = stream;
document.body.append(audio);
}
});
```
Note: when `receiveAudioStreamType` is `"only_composed"` and the participant is also sharing
their own audio, they will hear their own voice in the composed stream.
To receive both individual and composed streams, use `receiveVideoStreamType: "all"`. Individual
streams arrive via `remoteTrackStarted` and composed streams arrive via `composedTrackStarted`
(or `remoteTrackStarted` with `isComposedStream: true`).
---
## Server-Side Room Management Pattern (Node.js)
```javascript
const axios = require('axios');
const BASE_URL = 'https://yourapp.metered.live/api/v1';
const SECRET_KEY = process.env.METERED_SECRET_KEY;
// Create a room
async function createRoom(roomName, isPrivate) {
const response = await axios.post(`${BASE_URL}/room`, {
roomName: roomName,
privacy: isPrivate ? 'private' : 'public',
ejectAfterElapsedTimeInSec: 7200,
enableRecording: true
}, {
params: { secretKey: SECRET_KEY }
});
return response.data;
}
// Generate token for a user
async function generateToken(roomName, userName, isAdmin) {
const response = await axios.post(`${BASE_URL}/token`, {
roomName: roomName,
name: userName,
isAdmin: isAdmin
}, {
params: { secretKey: SECRET_KEY }
});
return response.data.token;
}
// Get active sessions
async function getActiveSessions() {
const response = await axios.get(`${BASE_URL}/rooms/active/meetingsessions`, {
params: { secretKey: SECRET_KEY }
});
return response.data;
}
// Get recordings for a room
async function getRoomRecordings(roomName) {
const response = await axios.get(`${BASE_URL}/recordings/room/${roomName}`, {
params: { secretKey: SECRET_KEY }
});
return response.data;
}
// Download a recording
async function getRecordingDownloadURL(recordingId) {
const response = await axios.get(`${BASE_URL}/recording/${recordingId}/download`, {
params: { secretKey: SECRET_KEY }
});
return response.data.url;
}
```