View a Call Stream
Join live WebRTC calls for ultra-low latency viewing.
There are two ways to view video streams:
- WebRTC Call Viewer (this guide): Join live calls for ultra-low latency (less than 1s)
- Manifest Player: View transcoded streams via HLS/FLV (see Play a Stream Using a Manifest)
This guide covers joining and viewing WebRTC calls, which is ideal for:
- Live interactive streaming
- Real-time collaboration
- Scenarios requiring sub-second latency
- React
- Vanilla JavaScript
Prerequisites
This guide assumes basic knowledge of React concepts. The component uses pre-built components (JoinCallButton, Peers) to simplify the implementation.
Peers Component
A reusable component that automatically discovers and displays all broadcasting peers in a WebRTC call. This component abstracts away the complexity of peer management, player creation, and stream handling. The PeersComponent is a building block that will be used in subsquent examples throught the documentation site.
Imports
Props
Hooks
The Peers component uses two key hooks:
useCallAPI(): Retrieves the call instance from React Context (provided by CallAPIProvider wrapper)useCallPeers(call): Automatically discovers all broadcasting peers in the call and returns an array of peer objects. This hook handles:- Listening for peer join/leave events
- Managing peer lifecycle
- Tracking which peers are broadcasting
- Cleaning up when peers disconnect
Render UI
The component renders dynamically based on peer availability:
- No peers: Returns null (renders nothing) when no broadcasters are in the call
- Active peers: Maps over the peers array to render each broadcaster with:
- PeerAPIProvider: Makes peer instance available to child components
- Player: Displays the peer's video stream
- PeerMutedBadge: Shows muted/unmuted audio status
- Display name: Shows the peer's name overlay
- Custom children: Any additional overlays passed as props
The UI automatically updates when peers join or leave the call.
Full Component Code
Call Viewer Component
Imports
In other examples we use the custom <CallControls/> component or useCallControlsHook; however, since this is a simpler implementation (simply viewing a call rather than viewing and broadcasting), we will use local state and components from the @video/video-client-react package directly.
Key imports:
- JoinCallButton
- EndCallButton
- CallAPIProvider
Props
Since we are viewing an existing call, a callId is required (in addition to authentication and backendUrl)
State
Set up state to track the call instance and create an authentication client:
Key Points:
- The call state tracks whether the user has joined the call
- The useAuthClient hook handles authentication and token refresh
- Token must have "viewer" or "private-viewer" scope
Render UI
The component has three conditional states:
- Loading: Show loading message while authentication initializes
- Not Joined: Show JoinCallButton to join the call
- Joined: Show Peers component and EndCallButton
How it works:
- JoinCallButton handles the complexity of joining the call
- Once joined, the Peers component automatically discovers and displays all broadcasters
- EndCallButton leaves the call and resets state
Full Component Code
Full Code
View A Call Stream Components
Supporting Components
The following components are used by the View a Call Stream component and are documented in View a Stream and Set Up A Livestream Video:
Viewing a Stream with Vanilla JavaScript
This guide demonstrates how to view a livestream by joining a WebRTC call using the @video/video-client-core library with vanilla JavaScript (no framework required).
What is a WebRTC Call Viewer?
There are two ways to view video streams:
- WebRTC Call Viewer (this guide): Join live calls for ultra-low latency (less than 1 second)
- Manifest Player: View transcoded streams via HLS/FLV (see View A Stream)
This guide covers joining and viewing WebRTC calls, which is ideal for:
- Live interactive streaming
- Real-time collaboration
- Scenarios requiring sub-second latency
Overview
The WebRTC call viewer application consists of three main files:
- player.html - HTML structure and script imports
- auth.js - Authentication configuration and client setup
- player-utils.js - Utility functions for creating player UI and controls
- player.js - Main application logic for joining calls and managing peer streams
Prerequisites
Before you begin, you'll need:
- Authentication Token: A JWT token with "viewer" or "private-viewer" scope (from Native Frame)
- Call ID: The unique identifier for the live call you want to join
- Backend Endpoint: Your Native Frame backend URL (e.g.,
https://your-subdomain.nativeframe.com)
The authentication token must be configured before the application loads. See the authentication setup section below.
Creating a Call Viewer with Vanilla JavaScript
1. HTML Structure
First, create an HTML file that loads the video-client-core library and your JavaScript modules.
<!--
Native Frame WebRTC Call Viewer - Vanilla JavaScript Implementation
This HTML file demonstrates how to view a livestream by joining a WebRTC call
using the @video/video-client-core library with vanilla JavaScript (no framework required).
WebRTC Call Viewer vs Manifest Player:
- WebRTC viewers join live calls for ultra-low latency (less than 1 second)
- Manifest players use HLS/FLV/DASH URLs for transcoded streams with higher latency
- WebRTC is ideal for real-time interaction and live collaboration
- Manifest players are better for VOD and CDN-delivered content
The WebRTC call viewer allows viewers to:
- Join live calls with a callId
- Automatically discover and display all broadcasting peers
- Control playback (play/pause, mute, volume) for each peer
- Leave the call at any time
Required Setup:
1. Obtain authentication token with "viewer" or "private-viewer" scope
2. Get the callId from your backend or broadcaster
3. Load the video-client-core library via CDN
4. Include the necessary JavaScript modules
-->
<!doctype html>
<html>
<head>
<!--
Import Map Configuration
This configures module resolution for ES modules. The "vdc-cdn" alias
points to the video-client-core library hosted on the Native Frame CDN.
You can update the version number in the URL to use a different version
of the library. Check https://cdn.nativeframe.com/ for available versions.
-->
<script type="importmap">
{
"imports": {
"vdc-cdn": "https://cdn.nativeframe.com/video-client-core-13.2.0.js"
}
}
</script>
<!--
JavaScript Module Imports
These modules must be loaded in order:
1. player-utils.js - Provides helper functions for creating player UI and controls
2. auth.js - Handles authentication with the Native Frame backend
3. player.js - Main application logic for joining calls and managing peer streams
Note: WebRTC call viewers require authentication (unlike manifest players)
-->
<script type="module" src="/js/player-utils.js"></script>
<script type="module" src="/js/auth.js"></script>
<script type="module" src="/js/player.js"></script>
</head>
<body>
<!--
Viewer UI Structure
The page consists of three main elements:
1. videoContainer - Will be populated with video players for each broadcasting peer
2. joinCallBtn - Button to join the call
3. endCallBtn - Button to leave the call (initially hidden)
When a user joins the call, video players are automatically created for each
peer that is broadcasting. Multiple peers can broadcast simultaneously, and
each will have their own video player with independent controls.
-->
<div>
<!-- Video players for broadcasting peers will be injected here -->
<div id="videoContainer"></div>
<!-- Join call button - visible by default -->
<button id="joinCallBtn">Join Call</button>
<!-- End call button - hidden until user joins the call -->
<button id="endCallBtn" style="display: none;">End Call</button>
</div>
</body>
</html>
Import Map Configuration
<script type="importmap">
{
"imports": {
"vdc-cdn": "https://cdn.nativeframe.com/video-client-core-13.2.0.js"
}
}
</script>
The import map tells the browser where to find the vdc-cdn module. You can update the version number in the URL to use different versions of the video-client-core library.
JavaScript Module Imports
These modules must be loaded as ES modules (type="module") and in this order:
player-utils.js- Provides UI helper functionsauth.js- Handles authentication with the Native Frame backendplayer.js- Contains main application logic
UI Structure
The page contains three main elements:
videoContainer: Will be populated with video players for each broadcasting peerjoinCallBtn: Button to join the callendCallBtn: Button to leave the call (initially hidden)
When a user joins the call, video players are automatically created for each peer that is broadcasting. Multiple peers can broadcast simultaneously, and each will have their own video player with independent controls.
2. Authentication Setup (auth.js)
The authentication module handles loading tokens and creating authenticated clients.
For comprehensive documentation on the authentication module, see the auth.js setup.
Important: The authentication token must have "viewer" or "private-viewer" scope (not "broadcaster" scope).
3. Player Utilities (player-utils.js)
This module provides utility functions for creating and managing player UI.
For comprehensive documentation on player utilities, see the player-utils.js setup.
The player-utils module provides:
appendVideoElement(id)- Creates and appends video element to DOMattachPlayerClickHandlers(player, id)- Sets up playback control event handlersremovePlayerClickHandlers(id)- Cleans up event listeners
4. Main Application Logic (player.js)
The main application file handles joining the call and managing peer streams.
Imports
import { removePlayerClickHandlers, appendVideoElement, attachPlayerClickHandlers } from './player-utils.js';
import { setAuthClient, backendEndpoint, viewerToken } from './auth.js';
import { joinCall, requestPlayer } from 'vdc-cdn';
Call ID Configuration
/**
* Call ID Configuration
*
* The callId identifies which live call to join. In a production application,
* you would typically:
* - Receive the callId from your backend API
* - Get it from a URL parameter (e.g., /watch?callId=abc123)
* - Fetch it from your video management system
* - Have the broadcaster share it with viewers
*
* Example ways to obtain callId:
* const callId = new URLSearchParams(window.location.search).get('callId');
* const callId = await fetch('/api/active-call').then(r => r.json()).then(d => d.callId);
*/
const callId = "your-call-id-here"; // Replace with your actual callId
The callId identifies which live call to join. In a production application, you would typically:
- Receive the callId from your backend API
- Get it from a URL parameter (e.g.,
/watch?callId=abc123) - Fetch it from your video management system
- Have the broadcaster share it with viewers
Application State
/**
* Application State Variables
*
* These module-level variables maintain the state of the viewer application:
*
* - call: The WebRTC call instance representing the connection to the live call
* - player: Reserved for future use (currently managed in peers array)
* - peers: Array of peer objects, each containing:
* - player: Player instance for displaying the peer's stream
* - stream: The media stream from the peer
* - peer: The peer object with metadata
* - streamName: Name of the stream (e.g., "default" for main camera/mic)
* - id: Unique identifier for the peer
* - authClient: Authenticated client for API requests
*
* All are initialized to null/empty and populated during the application lifecycle.
*/
let call = null;
let player = null;
let peers = [];
let authClient = null;
These module-level variables track:
call: The WebRTC call instancepeers: Array of peer objects (broadcasters in the call)authClient: Authenticated client for API requests
The peers array contains an object for each broadcasting peer:
player: Player instance for displaying the peer's streamstream: The media stream from the peerpeer: Peer metadatastreamName: Stream name (usually "default")id: Unique identifier
Handle Stream Added Event
/**
* Handle Stream Added Event
*
* This is called when a peer starts broadcasting. It creates a new player
* for the peer's stream and displays it in the UI.
*
* @param {Object} ev - Stream added event object
* @param {Object} ev.stream - The new stream to display
* @param {Object} ev.peer - The peer object with metadata
* @param {string} ev.streamName - Name of the stream (e.g., "default")
*
* Setup process:
* 1. Request a player for the new stream
* 2. Create a video element and append it to the DOM
* 3. Attach the player to the video element
* 4. Set up playback control event handlers
* 5. Add the peer to the peers array for tracking
*
* Multiple peers can broadcast simultaneously, and each will have their own
* player with independent controls.
*/
async function handleStreamAdded(ev) {
// Request a player for the new stream
// autoPlay: true - Start playing automatically when the stream is ready
// muted: false - Enable audio by default
const newPlayer = await requestPlayer(ev.stream, { autoPlay: true, muted: false });
// Get the unique identifier for this peer
const id = ev.stream.source?.id;
// Create a video element and append it to the DOM
const video = appendVideoElement(id);
// Attach the player to the video element
// This connects the media stream to the video tag for display
newPlayer.attachTo(video);
// Attach playback control event handlers (play/pause, mute, volume)
attachPlayerClickHandlers(newPlayer, id);
// Create a peer object to track this broadcaster
const newPeer = {
player: newPlayer, // Player instance for this peer
stream: ev.stream, // Media stream from the peer
peer: ev.peer, // Peer metadata
streamName: ev.streamName, // Stream name (usually "default")
id, // Unique identifier
};
// Add the new peer to the peers array
peers.push(newPeer);
}
This event is fired when a peer starts broadcasting. The handler:
- Requests a player for the new stream
- Creates a video element and appends it to the DOM
- Attaches the player to the video element
- Sets up playback control event handlers
- Adds the peer to the peers array for tracking
Multiple Peers: Multiple peers can broadcast simultaneously, and each will have their own player with independent controls.
Handle Stream Removed Event
/**
* Handle Stream Removed Event
*
* This is called when a peer stops broadcasting or leaves the call.
* It cleans up the player and removes the video element from the DOM.
*
* @param {Object} ev - Stream removed event object
* @param {Object} ev.stream - The stream that was removed
* @param {Object} ev.stream.source - Source information containing the peer ID
*
* Cleanup process:
* 1. Find the peer in the peers array by matching stream source ID
* 2. Detach the player from the video element
* 3. Dispose of the player to release resources
* 4. Remove the video wrapper element from the DOM
* 5. Remove the peer from the peers array
*/
function handleStreamRemoved(ev) {
// Find the peer by matching the stream source ID
const index = peers.findIndex((peer) => peer.stream.source?.id === ev.stream.source?.id);
if (index !== -1) {
(async () => {
// Detach the player from the video element
await peers[index].player.detach(true);
// Dispose of the player to release resources
await peers[index].player.dispose();
// Remove the video wrapper from the DOM
const wrapper = document.getElementById(`video-wrapper-${peers[index].id}`);
if (wrapper) {
wrapper.remove();
}
// Remove the peer from the array
peers.splice(index, 1);
})();
}
}
This event is fired when a peer stops broadcasting or leaves the call. The handler:
- Finds the peer in the peers array
- Detaches and disposes of the player
- Removes the video element from the DOM
- Removes the peer from the peers array
Attach Call Button Handlers
/**
* Attach Call Button Event Handlers
*
* Sets up click handlers for the join and end call buttons.
* These buttons control the viewer's connection to the live call.
*/
function attachCallButtonClickHandlers() {
/**
* Handle End Call
*
* Leaves the call and cleans up all resources.
* This is called when the user clicks the "End Call" button.
*
* Cleanup process:
* 1. Disable the end call button to prevent double-clicks
* 2. Remove stream event listeners from the call
* 3. Dispose of all peer players and remove their UI elements
* 4. Dispose of the call connection
* 5. Update UI to show the join call button again
*/
function handleEndCall() {
try {
// Disable button to prevent multiple clicks
document.getElementById("endCallBtn").disabled = true;
// Remove stream event listeners
call.off("streamRemoved", handleStreamRemoved);
// Clean up all peer players
peers.forEach(async (peer, index) => {
// Detach and dispose of the player
await peer.player.detach(true);
await peer.player.dispose();
// Remove the video wrapper from the DOM
const wrapper = document.getElementById(`video-wrapper-${peer.id}`);
if (wrapper) {
wrapper.remove();
}
// Remove from peers array
peers.splice(index, 1);
});
// Dispose of the call connection
call?.dispose("Call Disposed");
call = null;
// Update UI to show join button again
document.getElementById("joinCallBtn").disabled = false;
document.getElementById("joinCallBtn").style.display = "block";
document.getElementById("endCallBtn").style.display = "none";
} catch (error) {
console.error(error);
}
}
/**
* Handle Join Call
*
* Joins the live call and sets up event listeners for peer streams.
* This is called when the user clicks the "Join Call" button.
*
* Join process:
* 1. Disable the join call button to prevent duplicate joins
* 2. Call joinCall() with the callId and authentication
* 3. Update UI to show the end call button
* 4. Attach event listeners for streamAdded and streamRemoved
*
* Once joined, the call will automatically trigger streamAdded events
* for any peers that are currently broadcasting.
*
* Call Options:
* - user: Viewer identification (userId and displayName)
* - auth: Authentication client (must have "viewer" or "private-viewer" scope)
* - backendEndpoints: Array of backend URLs (with automatic failover)
*/
async function handleJoinCall() {
try {
// Disable the join call button to prevent multiple joins
document.getElementById("joinCallBtn").disabled = true;
// Join the call with the specified callId
call = await joinCall(callId, {
// User information for this viewer
user: { userId: "123", displayName: "John Doe" },
// Authentication client (with viewer scope)
auth: authClient,
// Backend endpoints to connect to (with automatic failover)
backendEndpoints: [backendEndpoint],
});
// Update UI to show end call button
document.getElementById("joinCallBtn").style.display = "none";
document.getElementById("endCallBtn").style.display = "block";
document.getElementById("endCallBtn").disabled = false;
// Attach event listeners for peer streams
// streamAdded: Fired when a peer starts broadcasting
// streamRemoved: Fired when a peer stops broadcasting or leaves
call.on("streamAdded", handleStreamAdded);
call.on("streamRemoved", handleStreamRemoved);
} catch (error) {
console.error(error);
}
}
/**
* Wire Up Event Listeners
*
* Connect the handler functions to the call button elements
*/
document.getElementById("joinCallBtn").onclick = handleJoinCall;
document.getElementById("endCallBtn").onclick = handleEndCall;
}
Sets up click handlers for the join and end call buttons:
Join Call Handler:
- Disables the join button to prevent duplicate joins
- Calls
joinCall()with the callId and authentication - Updates UI to show the end call button
- Attaches event listeners for
streamAddedandstreamRemoved
End Call Handler:
- Disables the end button to prevent double-clicks
- Removes stream event listeners
- Disposes of all peer players
- Disposes of the call connection
- Updates UI to show the join button again
Join Call Options:
user: Viewer identification (userId and displayName)auth: Authentication client (must have "viewer" or "private-viewer" scope)backendEndpoints: Array of backend URLs (with automatic failover)
Initialize the Viewer
/**
* Initialize the Call Viewer Application
*
* This is the main initialization function that sets up the viewer.
* It runs when the page loads and performs these steps:
*
* 1. Creates an authenticated client using the viewer token
* 2. Attaches click handlers to the join and end call buttons
*
* After init() completes, the user can click "Join Call" to connect to
* the live call and start viewing broadcasting peers.
*
* Important: The viewerToken must have "viewer" or "private-viewer" scope.
*/
async function init() {
// Create authenticated client for API requests
authClient = await setAuthClient(viewerToken);
// Set up click handlers for call buttons
attachCallButtonClickHandlers();
}
The init() function sets up the viewer:
- Creates an authenticated client using the viewer token
- Attaches click handlers to the call buttons
After init() completes, the user can click "Join Call" to connect to the live call and start viewing broadcasting peers.
Clean Up Resources
/**
* Clean Up All Resources
*
* Disposes of all resources when the page is being unloaded.
* This is called automatically by the beforeunload event.
*
* Cleanup process:
* 1. Dispose of the main player (if any)
* 2. Dispose of the call connection
* 3. Dispose of all peer players
* 4. Remove all video elements from the DOM
* 5. Remove event listeners from buttons
*
* Important: Always dispose of video-client resources when done to:
* - Release WebRTC connections
* - Stop media streams
* - Free up memory
* - Prevent resource leaks
*/
function dispose() {
// Dispose of the main player
player?.dispose();
player = null;
// Dispose of the call connection
call?.dispose();
call = null;
// Dispose of all peer players
peers.forEach(async (peer, index) => {
// Detach and dispose of each player
await peer.player.detach(true);
await peer.player.dispose();
// Remove the video wrapper from the DOM
const wrapper = document.getElementById(`video-wrapper-${peer.stream.source?.id}`);
if (wrapper) {
wrapper.remove();
}
// Remove from peers array
peers.splice(index, 1);
});
// Remove event listeners from call buttons
document.getElementById("joinCallBtn")?.removeAllListeners();
document.getElementById("endCallBtn")?.removeAllListeners();
// Remove player control event listeners
removePlayerClickHandlers();
}
/**
* Register Cleanup Handler
*
* Sets up an event listener to clean up resources when the page unloads.
* This ensures WebRTC connections and media streams are properly released.
*/
window.addEventListener("beforeunload", dispose);
Properly disposes of all resources when the page unloads:
- Releases WebRTC connections
- Stops media streams
- Frees up memory
- Prevents resource leaks
Important: Always dispose of video-client resources when done!
Application Entry Point
/**
* Application Entry Point
*
* This runs when the page finishes loading. It calls init() to set up
* the viewer and make it ready to join calls.
*/
window.onload = init;
The application starts when the page loads by calling init().
5. Usage Flow
Once everything is set up, the typical usage flow is:
- Page loads →
init()runs, authentication is set up - User clicks "Join Call" → Connection to call is established
- streamAdded events → Players are created for broadcasting peers
- User controls playback → Play/pause, mute, volume controls for each peer
- streamRemoved events → Players are cleaned up when peers leave
- User clicks "End Call" → Call ends and all resources are cleaned up
5. Full Code
<!--
Native Frame WebRTC Call Viewer - Vanilla JavaScript Implementation
This HTML file demonstrates how to view a livestream by joining a WebRTC call
using the @video/video-client-core library with vanilla JavaScript (no framework required).
WebRTC Call Viewer vs Manifest Player:
- WebRTC viewers join live calls for ultra-low latency (less than 1 second)
- Manifest players use HLS/FLV/DASH URLs for transcoded streams with higher latency
- WebRTC is ideal for real-time interaction and live collaboration
- Manifest players are better for VOD and CDN-delivered content
The WebRTC call viewer allows viewers to:
- Join live calls with a callId
- Automatically discover and display all broadcasting peers
- Control playback (play/pause, mute, volume) for each peer
- Leave the call at any time
Required Setup:
1. Obtain authentication token with "viewer" or "private-viewer" scope
2. Get the callId from your backend or broadcaster
3. Load the video-client-core library via CDN
4. Include the necessary JavaScript modules
-->
<!doctype html>
<html>
<head>
<!--
Import Map Configuration
This configures module resolution for ES modules. The "vdc-cdn" alias
points to the video-client-core library hosted on the Native Frame CDN.
You can update the version number in the URL to use a different version
of the library. Check https://cdn.nativeframe.com/ for available versions.
-->
<script type="importmap">
{
"imports": {
"vdc-cdn": "https://cdn.nativeframe.com/video-client-core-13.2.0.js"
}
}
</script>
<!--
JavaScript Module Imports
These modules must be loaded in order:
1. player-utils.js - Provides helper functions for creating player UI and controls
2. auth.js - Handles authentication with the Native Frame backend
3. player.js - Main application logic for joining calls and managing peer streams
Note: WebRTC call viewers require authentication (unlike manifest players)
-->
<script type="module" src="/js/player-utils.js"></script>
<script type="module" src="/js/auth.js"></script>
<script type="module" src="/js/player.js"></script>
</head>
<body>
<!--
Viewer UI Structure
The page consists of three main elements:
1. videoContainer - Will be populated with video players for each broadcasting peer
2. joinCallBtn - Button to join the call
3. endCallBtn - Button to leave the call (initially hidden)
When a user joins the call, video players are automatically created for each
peer that is broadcasting. Multiple peers can broadcast simultaneously, and
each will have their own video player with independent controls.
-->
<div>
<!-- Video players for broadcasting peers will be injected here -->
<div id="videoContainer"></div>
<!-- Join call button - visible by default -->
<button id="joinCallBtn">Join Call</button>
<!-- End call button - hidden until user joins the call -->
<button id="endCallBtn" style="display: none;">End Call</button>
</div>
</body>
</html>
/**
* Authentication Module for Native Frame Encoder
*
* This module handles authentication with the Native Frame backend. It manages:
* - Creating authenticated clients for API requests
*
* Prerequisites:
* - JWT authentication token
*
*/
import { BaseAuthClient } from 'vdc-cdn';
/**
* Create an Authenticated Client
*
* This function creates a BaseAuthClient instance that will be used to authenticate
* all API requests when creating calls and broadcasts.
*
* @param {string} token - JWT authentication token (broadcasterToken or viewerToken)
* @returns {Promise<BaseAuthClient>} Authenticated client instance
* @throws {Error} If token is empty or invalid
*
* Usage:
* const authClient = await setAuthClient(broadcasterToken);
*
* The returned authClient is passed to createCall() to authenticate the connection.
*/
export async function setAuthClient(token) {
// Validate that a token was provided
if (token.length === 0) {
throw new Error("No JWT found");
}
// Create and return the authenticated client
// BaseAuthClient handles token validation and API request authentication
const auth = new BaseAuthClient(token);
return auth;
}
/**
* Player Utilities Module
*
* This module provides utility functions for creating and managing manifest player UI.
* It handles:
* - Creating video elements and playback controls
* - Appending player UI to the DOM
* - Attaching event handlers for player controls
* - Removing event handlers on cleanup
*
* Key Concepts:
* - Player: The video player instance that manages playback of manifest streams
* - Video Element: The HTML <video> tag where the stream is displayed
* - Playback Controls: Buttons and inputs for play/pause, mute, and volume control
*/
/**
* Attach Event Handlers to Player Controls
*
* Sets up click and change event handlers for all player UI controls.
* This enables users to:
* - Play/pause video playback
* - Mute/unmute audio
* - Adjust volume level
*
* @param {Player} player - The player instance to control
* @param {string} id - Unique identifier for the video element and its controls
*
* The player object provides these key properties:
* - localVideoPaused: Boolean indicating if video is paused
* - localAudioMuted: Boolean indicating if audio is muted
* - localAudioVolume: Number (0-1) representing volume level
*/
function attachPlayerClickHandlers(player, id) {
/**
* Toggle Play/Pause
*
* Toggles between playing and pausing the video stream.
* The player.localVideoPaused property indicates current playback state.
*
* @param {Event} event - Click event from the play button
*/
async function togglePlay(event) {
if (player.localVideoPaused) {
// Resume playback
await player.play();
event.target.textContent = "Pause";
} else {
// Pause playback
await player.pause();
event.target.textContent = "Play";
}
}
/**
* Toggle Mute/Unmute
*
* Toggles audio muting on and off.
* When muted, the video continues playing but no audio is heard.
*
* @param {Event} event - Click event from the mute button
*/
async function toggleMute(event) {
if (player.localAudioMuted) {
// Unmute audio
await player.unmute();
event.target.textContent = "Mute";
} else {
// Mute audio
await player.mute();
event.target.textContent = "Unmute";
}
}
/**
* Handle Volume Change
*
* Adjusts the audio volume based on slider input.
* Volume is converted from 0-100 range to 0-1 range.
* Setting volume to 0 automatically mutes the player.
*
* @param {Event} ev - Change event from the volume slider
*/
function handleVolume(ev) {
const volumeValue = Number(ev.target.value);
if (volumeValue === 0) {
// Mute when volume is set to 0
player.localAudioMuted = true;
} else {
// Unmute when volume is above 0
player.localAudioMuted = false;
}
// Set volume (convert from 0-100 to 0-1)
player.localAudioVolume = volumeValue / 100;
}
/**
* Wire Up Event Listeners
*
* Connect the handler functions to the corresponding DOM elements.
* Sets initial button text to reflect starting player state.
*/
document.getElementById(`playBtn-${id}`).onclick = togglePlay;
document.getElementById(`playBtn-${id}`).textContent = "Pause"; // Default state is playing
document.getElementById(`muteBtn-${id}`).onclick = toggleMute;
document.getElementById(`muteBtn-${id}`).textContent = "Mute"; // Default state is unmuted
document.getElementById(`volume-${id}`).onchange = handleVolume;
}
/**
* Remove Event Listeners from Player Controls
*
* Cleans up event listeners when the player is disposed.
* This prevents memory leaks and ensures proper resource cleanup.
*
* @param {string} id - Unique identifier for the video element and its controls
*
* Important: Always call this function before disposing of a player to
* properly clean up event handlers and prevent memory leaks.
*/
function removePlayerClickHandlers(id) {
// Remove event listeners from all control elements
document.getElementById(`playBtn-${id}`).removeAllListeners();
document.getElementById(`muteBtn-${id}`).removeAllListeners();
document.getElementById(`volume-${id}`).removeAllListeners();
}
/**
* Generate HTML for Video Player and Controls
*
* Creates the HTML structure for the video element and playback controls.
* This includes:
* - Video element for displaying the stream
* - Play/pause toggle button
* - Mute/unmute toggle button
* - Volume slider control
*
* @param {string} id - Unique identifier for the video element and its controls
* @returns {string} HTML string containing the player UI structure
*
* The generated HTML will be injected into the videoContainer div.
* Each control element is given a unique ID based on the provided id parameter,
* allowing multiple players on the same page.
*/
function createVideoElement(id) {
return `
<div id="video-wrapper-${id}">
<!-- Video element where the stream will be displayed -->
<video
width="100%"
height="100%"
id="${id}"
>
</video>
<!-- Play/pause toggle button -->
<button id="playBtn-${id}"></button>
<!-- Mute/unmute toggle button -->
<button id="muteBtn-${id}"></button>
<!-- Volume control slider -->
<div class="volume-container">
<label for="volume-${id}">Volume: </label>
<input type="range" id="volume-${id}" min="0" max="100" value="50" />
</div>
</div>
`;
}
/**
* Append Video Player to DOM
*
* Injects the player UI into the page and returns a reference to the video element.
* This function:
* 1. Finds the video container div
* 2. Inserts the player HTML
* 3. Returns the video element for player attachment
*
* @param {string} id - Unique identifier for the video element
* @returns {HTMLVideoElement} Reference to the video element
*
* The video element is returned so the Player can be attached to it using
* player.attachTo(videoElement).
*/
function appendVideoElement(id) {
// Find the container where we'll inject the player UI
const videoContainer = document.getElementById("videoContainer");
// Insert the player HTML into the container
videoContainer.insertAdjacentHTML("beforeend", createVideoElement(id));
// Return reference to the video element
return document.getElementById(id);
}
/**
* Export Utility Functions
*
* These functions are used by the main manifest-player.js module to
* create and manage the player UI.
*/
export { attachPlayerClickHandlers, removePlayerClickHandlers, appendVideoElement };
/**
* Native Frame WebRTC Call Viewer - Main Application
*
* This is the main application file for the WebRTC call viewer. It handles:
* - Joining live WebRTC calls
* - Discovering and displaying broadcasting peers
* - Managing multiple peer streams
* - Handling peer join/leave events
* - Resource cleanup
*
* Flow:
* 1. Page loads → init() is called
* 2. Authentication client is created
* 3. User clicks "Join Call" → Call is joined
* 4. streamAdded events → Players are created for each broadcasting peer
* 5. streamRemoved events → Players are cleaned up when peers leave
* 6. User clicks "End Call" → Call ends and all resources are cleaned up
*
* WebRTC Call Viewer vs Manifest Player:
* - WebRTC viewers connect directly to live calls for ultra-low latency (< 1s)
* - Manifest players use HLS/FLV URLs for transcoded streams (3-30s latency)
* - WebRTC requires authentication and a callId
* - WebRTC is ideal for real-time interaction and collaboration
*/
import { removePlayerClickHandlers, appendVideoElement, attachPlayerClickHandlers } from './player-utils.js';
import { setAuthClient, backendEndpoint, viewerToken } from './auth.js';
import { joinCall, requestPlayer } from 'vdc-cdn';
/**
* Call ID Configuration
*
* The callId identifies which live call to join. In a production application,
* you would typically:
* - Receive the callId from your backend API
* - Get it from a URL parameter (e.g., /watch?callId=abc123)
* - Fetch it from your video management system
* - Have the broadcaster share it with viewers
*
* Example ways to obtain callId:
* const callId = new URLSearchParams(window.location.search).get('callId');
* const callId = await fetch('/api/active-call').then(r => r.json()).then(d => d.callId);
*/
const callId = "your-call-id-here"; // Replace with your actual callId
/**
* Application State Variables
*
* These module-level variables maintain the state of the viewer application:
*
* - call: The WebRTC call instance representing the connection to the live call
* - player: Reserved for future use (currently managed in peers array)
* - peers: Array of peer objects, each containing:
* - player: Player instance for displaying the peer's stream
* - stream: The media stream from the peer
* - peer: The peer object with metadata
* - streamName: Name of the stream (e.g., "default" for main camera/mic)
* - id: Unique identifier for the peer
* - authClient: Authenticated client for API requests
*
* All are initialized to null/empty and populated during the application lifecycle.
*/
let call = null;
let player = null;
let peers = [];
let authClient = null;
/**
* Handle Stream Removed Event
*
* This is called when a peer stops broadcasting or leaves the call.
* It cleans up the player and removes the video element from the DOM.
*
* @param {Object} ev - Stream removed event object
* @param {Object} ev.stream - The stream that was removed
* @param {Object} ev.stream.source - Source information containing the peer ID
*
* Cleanup process:
* 1. Find the peer in the peers array by matching stream source ID
* 2. Detach the player from the video element
* 3. Dispose of the player to release resources
* 4. Remove the video wrapper element from the DOM
* 5. Remove the peer from the peers array
*/
function handleStreamRemoved(ev) {
// Find the peer by matching the stream source ID
const index = peers.findIndex((peer) => peer.stream.source?.id === ev.stream.source?.id);
if (index !== -1) {
(async () => {
// Detach the player from the video element
await peers[index].player.detach(true);
// Dispose of the player to release resources
await peers[index].player.dispose();
// Remove the video wrapper from the DOM
const wrapper = document.getElementById(`video-wrapper-${peers[index].id}`);
if (wrapper) {
wrapper.remove();
}
// Remove the peer from the array
peers.splice(index, 1);
})();
}
}
/**
* Handle Stream Added Event
*
* This is called when a peer starts broadcasting. It creates a new player
* for the peer's stream and displays it in the UI.
*
* @param {Object} ev - Stream added event object
* @param {Object} ev.stream - The new stream to display
* @param {Object} ev.peer - The peer object with metadata
* @param {string} ev.streamName - Name of the stream (e.g., "default")
*
* Setup process:
* 1. Request a player for the new stream
* 2. Create a video element and append it to the DOM
* 3. Attach the player to the video element
* 4. Set up playback control event handlers
* 5. Add the peer to the peers array for tracking
*
* Multiple peers can broadcast simultaneously, and each will have their own
* player with independent controls.
*/
async function handleStreamAdded(ev) {
// Request a player for the new stream
// autoPlay: true - Start playing automatically when the stream is ready
// muted: false - Enable audio by default
const newPlayer = await requestPlayer(ev.stream, { autoPlay: true, muted: false });
// Get the unique identifier for this peer
const id = ev.stream.source?.id;
// Create a video element and append it to the DOM
const video = appendVideoElement(id);
// Attach the player to the video element
// This connects the media stream to the video tag for display
newPlayer.attachTo(video);
// Attach playback control event handlers (play/pause, mute, volume)
attachPlayerClickHandlers(newPlayer, id);
// Create a peer object to track this broadcaster
const newPeer = {
player: newPlayer, // Player instance for this peer
stream: ev.stream, // Media stream from the peer
peer: ev.peer, // Peer metadata
streamName: ev.streamName, // Stream name (usually "default")
id, // Unique identifier
};
// Add the new peer to the peers array
peers.push(newPeer);
}
/**
* Attach Call Button Event Handlers
*
* Sets up click handlers for the join and end call buttons.
* These buttons control the viewer's connection to the live call.
*/
function attachCallButtonClickHandlers() {
/**
* Handle End Call
*
* Leaves the call and cleans up all resources.
* This is called when the user clicks the "End Call" button.
*
* Cleanup process:
* 1. Disable the end call button to prevent double-clicks
* 2. Remove stream event listeners from the call
* 3. Dispose of all peer players and remove their UI elements
* 4. Dispose of the call connection
* 5. Update UI to show the join call button again
*/
function handleEndCall() {
try {
// Disable button to prevent multiple clicks
document.getElementById("endCallBtn").disabled = true;
// Remove stream event listeners
call.off("streamRemoved", handleStreamRemoved);
// Clean up all peer players
peers.forEach(async (peer, index) => {
// Detach and dispose of the player
await peer.player.detach(true);
await peer.player.dispose();
// Remove the video wrapper from the DOM
const wrapper = document.getElementById(`video-wrapper-${peer.id}`);
if (wrapper) {
wrapper.remove();
}
// Remove from peers array
peers.splice(index, 1);
});
// Dispose of the call connection
call?.dispose("Call Disposed");
call = null;
// Update UI to show join button again
document.getElementById("joinCallBtn").disabled = false;
document.getElementById("joinCallBtn").style.display = "block";
document.getElementById("endCallBtn").style.display = "none";
} catch (error) {
console.error(error);
}
}
/**
* Handle Join Call
*
* Joins the live call and sets up event listeners for peer streams.
* This is called when the user clicks the "Join Call" button.
*
* Join process:
* 1. Disable the join call button to prevent duplicate joins
* 2. Call joinCall() with the callId and authentication
* 3. Update UI to show the end call button
* 4. Attach event listeners for streamAdded and streamRemoved
*
* Once joined, the call will automatically trigger streamAdded events
* for any peers that are currently broadcasting.
*
* Call Options:
* - user: Viewer identification (userId and displayName)
* - auth: Authentication client (must have "viewer" or "private-viewer" scope)
* - backendEndpoints: Array of backend URLs (with automatic failover)
*/
async function handleJoinCall() {
try {
// Disable the join call button to prevent multiple joins
document.getElementById("joinCallBtn").disabled = true;
// Join the call with the specified callId
call = await joinCall(callId, {
// User information for this viewer
user: { userId: "123", displayName: "John Doe" },
// Authentication client (with viewer scope)
auth: authClient,
// Backend endpoints to connect to (with automatic failover)
backendEndpoints: [backendEndpoint],
});
// Update UI to show end call button
document.getElementById("joinCallBtn").style.display = "none";
document.getElementById("endCallBtn").style.display = "block";
document.getElementById("endCallBtn").disabled = false;
// Attach event listeners for peer streams
// streamAdded: Fired when a peer starts broadcasting
// streamRemoved: Fired when a peer stops broadcasting or leaves
call.on("streamAdded", handleStreamAdded);
call.on("streamRemoved", handleStreamRemoved);
} catch (error) {
console.error(error);
}
}
/**
* Wire Up Event Listeners
*
* Connect the handler functions to the call button elements
*/
document.getElementById("joinCallBtn").onclick = handleJoinCall;
document.getElementById("endCallBtn").onclick = handleEndCall;
}
/**
* Initialize the Call Viewer Application
*
* This is the main initialization function that sets up the viewer.
* It runs when the page loads and performs these steps:
*
* 1. Creates an authenticated client using the viewer token
* 2. Attaches click handlers to the join and end call buttons
*
* After init() completes, the user can click "Join Call" to connect to
* the live call and start viewing broadcasting peers.
*
* Important: The viewerToken must have "viewer" or "private-viewer" scope.
*/
async function init() {
// Create authenticated client for API requests
authClient = await setAuthClient(viewerToken);
// Set up click handlers for call buttons
attachCallButtonClickHandlers();
}
/**
* Clean Up All Resources
*
* Disposes of all resources when the page is being unloaded.
* This is called automatically by the beforeunload event.
*
* Cleanup process:
* 1. Dispose of the main player (if any)
* 2. Dispose of the call connection
* 3. Dispose of all peer players
* 4. Remove all video elements from the DOM
* 5. Remove event listeners from buttons
*
* Important: Always dispose of video-client resources when done to:
* - Release WebRTC connections
* - Stop media streams
* - Free up memory
* - Prevent resource leaks
*/
function dispose() {
// Dispose of the main player
player?.dispose();
player = null;
// Dispose of the call connection
call?.dispose();
call = null;
// Dispose of all peer players
peers.forEach(async (peer, index) => {
// Detach and dispose of each player
await peer.player.detach(true);
await peer.player.dispose();
// Remove the video wrapper from the DOM
const wrapper = document.getElementById(`video-wrapper-${peer.stream.source?.id}`);
if (wrapper) {
wrapper.remove();
}
// Remove from peers array
peers.splice(index, 1);
});
// Remove event listeners from call buttons
document.getElementById("joinCallBtn")?.removeAllListeners();
document.getElementById("endCallBtn")?.removeAllListeners();
// Remove player control event listeners
removePlayerClickHandlers();
}
/**
* Register Cleanup Handler
*
* Sets up an event listener to clean up resources when the page unloads.
* This ensures WebRTC connections and media streams are properly released.
*/
window.addEventListener("beforeunload", dispose);
/**
* Application Entry Point
*
* This runs when the page finishes loading. It calls init() to set up
* the viewer and make it ready to join calls.
*/
window.onload = init;
Key Concepts
WebRTC Call
A WebRTC call is a real-time connection between multiple participants:
- Ultra-low latency: Less than 1 second delay
- Peer-to-peer: Direct connection between participants
- Automatic discovery: New peers are automatically detected via
streamAddedevents - Authentication required: Viewers need a token with "viewer" or "private-viewer" scope
Call ID
The callId uniquely identifies a live call:
- Generated when a broadcaster creates a call
- Shared with viewers who want to join
- Required parameter for
joinCall() - Can be obtained from URL, API, or broadcaster
Stream Events
The call instance fires events when peers join/leave:
streamAdded Event:
- Fired when a peer starts broadcasting
- Contains the stream, peer info, and stream name
- Viewer creates a player to display the stream
streamRemoved Event:
- Fired when a peer stops broadcasting or leaves
- Contains information to identify which stream was removed
- Viewer cleans up the corresponding player
Peers Array
The peers array tracks all broadcasting participants:
- Each peer has their own player instance
- Each peer's video is displayed independently
- Peers can join and leave dynamically
- UI updates automatically as peers change
WebRTC Call Viewer vs Manifest Player
WebRTC Call Viewer:
- Connects directly to live calls
- Requires authentication and callId
- Ultra-low latency (< 1 second)
- Ideal for real-time interaction
- Automatic peer discovery
- Dynamic participant management
Manifest Player:
- Uses HLS/FLV/DASH URLs
- No authentication required
- Higher latency (3-30 seconds)
- Better for VOD and CDN content
- Adaptive bitrate streaming
- Single stream playback
Next Steps
Now that you have a basic call viewer, you can:
- Customize the Peers component styling
- Add custom overlays for each peer
- Implement additional player controls
- Handle peer events for notifications