# Metered — Complete API & SDK Reference (last updated: 2026-05-05) > Metered provides real-time communication infrastructure: a Video/Voice SDK, TURN Server Service, and Global Cloud SFU. This file combines all product references into one document. For individual product files, see: https://www.metered.ca/docs/llms.txt --- ## Part 1: Video SDK # 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