Set Up a Livestream Video
Learn how to broadcast audio and video from your application.
In order to livestream video, we must first set up a preview video. The preview video allows broadcasters to see exactly what their viewers will see before and during the livestream. It displays the output from the selected camera and provides controls for managing audio and video devices, ensuring everything looks and sounds correct before going live.
Prerequisites
Before getting started, in order to broadcast a livestream you will need the following:
/**
* Authentication token from your auth system
* This proves the user has permission to create calls and broadcast
*/
token: string;
/**
* Backend API endpoint URL (provided by Native Frame)
* Example: "https://api.nativeframe.com"
*/
backendEndpoint: string;
/**
* Unique identifier for this broadcast stream
* Should be unique per broadcaster
*/
streamKey: string;
Key Concepts
MediaStreamController
Manages access to camera and microphone:
- Controls device selection
- Toggles camera on/off (
videoPausedproperty) - Toggles microphone mute (
audioMutedproperty) - Switches between multiple devices
PreviewPlayer
Displays the local video feed before/during broadcast:
- Automatically plays when attached to a video element
- Should be muted to prevent audio feedback
- Must be disposed when done
Call
Represents the connection to the Native Frame backend:
- Created with
createCall(options) - Required before broadcasting can start
- Handles automatic reconnection and failover
- Disposes of active broadcasts when disposed
Broadcast
Represents an active broadcast stream:
- Created with
call.broadcast(mediaStreamController, options) - Sends audio/video to viewers
- Can be stopped without ending the call
- Identified by
streamName(usually "default")
This guide demonstrates how to set up a livestream preivew player using vanilla JavaScript with the @video/video-client-core library loaded from CDN. No framework required!
The preview player application consists of three main files:
- previewPlayer.html - HTML structure and script imports
- auth.js - Authentication configuration and client setup
- preview-player-utils.js - Preview Player UI and media device management
- preview-player.js - Main application logic for calls and broadcasts
HTML
First, create an HTML file that loads the video-client-core library and your JavaScript modules.
The page contains three main elements:
encoderContainer: Will be populated with video preview and device controlscallBtn: Button to create/end the call connectionbroadcastBtn: Button to start/stop broadcasting
<!--
Native Frame Preview Player - Vanilla JavaScript Implementation
This HTML file demonstrates how to set up a livestream preview player using the
@video/video-client-core library with vanilla JavaScript (no framework required).
The preview player allows a broadcaster to:
- Access camera and microphone
- Preview their video feed
-->
<!doctype html>
<html>
<head>
<!-- << importmap -->
<!--
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>
<!-- << end-importmap -->
<!-- << imports -->
<!--
JavaScript Module Imports
These modules must be loaded in order:
1. auth.js - Handles authentication with the Native Frame backend
2. encoder-utils.js - Provides helper functions for setting up the encoder UI
3. encoder.js - Main application logic for managing calls and broadcasts
-->
<script type="module" src="/js/auth.js"></script>
<script type="module" src="/js/encoder-utils.js"></script>
<script type="module" src="/js/encoder.js"></script>
<!-- << end-imports -->
</head>
<body>
<!--
Encoder UI Structure
The page consists of three main elements:
1. encoderContainer - Will be populated with the video preview and device controls
2. callBtn - Button to create/end the call connection
3. broadcastBtn - Button to start/stop broadcasting the stream
These elements are manipulated by the JavaScript modules to show/hide controls
based on the current state of the encoder.
-->
<div>
<!-- Video preview and device controls will be injected here -->
<div id="encoderContainer"></div>
<!-- Initially hidden, shown after encoder initialization -->
<button id="callBtn" style="display: none;">Start Call</button>
<!-- Initially hidden, shown after call is created -->
<button id="broadcastBtn" style="display: none;"></button>
</div>
</body>
</html>
Using JavaScript Modules
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.
<!--
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>
These modules must be loaded as ES modules (type="module") and in this order:
auth.js- Sets up authenticationpreview-player-utils.js- Provides UI helper functionspreview-player.js- Contains main application logic
<!--
JavaScript Module Imports
These modules must be loaded in order:
1. auth.js - Handles authentication with the Native Frame backend
2. encoder-utils.js - Provides helper functions for setting up the encoder UI
3. encoder.js - Main application logic for managing calls and broadcasts
-->
<script type="module" src="/js/auth.js"></script>
<script type="module" src="/js/encoder-utils.js"></script>
<script type="module" src="/js/encoder.js"></script>
JavaScript
Authentication Setup (auth.js)
The authentication module handles loading tokens and creating authenticated clients.
For more information on auth tokens, please see our Token Guide
The setAuthClient() function creates a BaseAuthClient instance that authenticates API requests when creating calls and broadcasts. This is a utility module that will be used in other demos.
/**
* 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 = await new BaseAuthClient(token);
return auth;
}
Preview Player Utils
This module handles requesting media devices and creating the preview player UI. This is also a utility module that will be used in other demos.
/**
* Encoder Utilities Module
*
* This module provides utility functions for setting up the video encoder UI.
* It handles:
* - Requesting access to camera and microphone
* - Creating the media stream controller
* - Setting up the preview player
* - Creating and managing the encoder UI elements
* - Attaching event handlers for device controls
*
* Key Concepts:
* - MediaStreamController: Manages access to camera/microphone and controls their state
* - PreviewPlayer: Displays the local video feed before broadcasting
* - MediaController: Global singleton that manages device enumeration and permissions
*/
import { requestPlayer, mediaController } from 'vdc-cdn'
// << request-encoder
/**
* Initialize Encoder with Media Devices
*
* This is the main initialization function for the encoder. It:
* 1. Requests camera and microphone permissions
* 2. Creates a MediaStreamController to manage the devices
* 3. Creates a PreviewPlayer to display the video
* 4. Builds the UI for device controls
* 5. Attaches event handlers for user interactions
*
* @returns {Promise<[MediaStreamController, PreviewPlayer]>} Array containing the controller and player
*
* Usage:
* const [mediaStreamController, previewPlayer] = await requestEncoder();
*
* The returned objects are used throughout the application:
* - mediaStreamController: Used to start/stop camera/mic and switch devices
* - previewPlayer: Used to display the video feed and must be disposed when done
*/
export async function requestEncoder() {
// Initialize the global media controller
// This requests browser permissions for camera and microphone access
await mediaController.init();
// Create a MediaStreamController instance
// This manages the actual media streams from the selected devices
const mediaStreamController = await mediaController.requestController();
// Enumerate available audio and video devices
// Returns arrays of available cameras and microphones
const [audioDevices, videoDevices] = getDevices();
// Create a preview player to display the local video feed
// autoPlay: true - automatically starts playing when attached
// muted: true - mutes the local preview (prevents audio feedback)
const previewPlayer = await requestPlayer(mediaStreamController, { autoPlay: true, muted: true });
// Create the video element and append it to the DOM
// Returns a reference to the video element
const video = appendEncoderHTMLToDOM();
// Attach the preview player to the video element
// This connects the media stream to the video tag for display
previewPlayer.attachTo(video);
// Set up click handlers for camera/mic toggle buttons and device selection
attachEncoderEventHandlers(mediaStreamController);
// Configure the initial devices (first available camera/mic)
// and populate the device selection dropdowns
setInitialDevices(mediaStreamController, audioDevices, videoDevices);
// Return both objects for use in the main application
return [mediaStreamController, previewPlayer];
}
// << end-request-encoder
// << create-encoder-html
/**
* Generate HTML for Encoder UI
*
* Creates the HTML structure for the video preview and device controls.
* This includes:
* - Video element for displaying the preview
* - Camera toggle button
* - Microphone toggle button
* - Video device selector dropdown
* - Audio device selector dropdown
*
* @returns {string} HTML string containing the encoder UI structure
*
* The generated HTML will be injected into the #encoderContainer div.
*/
export function createEncoderHTML() {
return `
<div class="video-wrapper">
<!-- Video element where the preview will be displayed -->
<video id="preview-player" style="height: 100%; width: 100%"></video>
<div class="controls">
<!-- Toggle camera on/off -->
<button id="cameraBtn"></button>
<!-- Toggle microphone on/off -->
<button id="micBtn"></button>
<!-- Select video input device (camera) -->
<label for="videoDeviceSelect">
Choose video device:
<select id="videoDeviceSelect" name="videoDeviceSelect"></select>
</label>
<!-- Select audio input device (microphone) -->
<label for="audioDeviceSelect">
Choose audio device:
<select id="audioDeviceSelect" name="audioDeviceSelect"></select>
</label>
</div>
</div>
`;
}
// << end-create-encoder-html
// << append-encoder-html
/**
* Append Encoder HTML to DOM
*
* Injects the encoder UI into the page and returns a reference to the video element.
* This function:
* 1. Finds the encoder container div
* 2. Inserts the encoder HTML
* 3. Returns the video element for player attachment
*
* @returns {HTMLVideoElement} Reference to the video element
*
* The video element is returned so the PreviewPlayer can be attached to it.
*/
function appendEncoderHTMLToDOM() {
// Find the container where we'll inject the encoder UI
const encoderContainer = document.getElementById("encoderContainer");
// Insert the encoder HTML into the container
encoderContainer.insertAdjacentHTML("beforeend", createEncoderHTML());
// Return reference to the video element
return document.getElementById("preview-player");
}
// << end-append-encoder-html
// << get-devices
/**
* Get Available Media Devices
*
* Retrieves lists of available audio and video input devices from the mediaController.
* This includes all cameras and microphones that the user has granted permission to access.
*
* @returns {[MediaDeviceInfo[], MediaDeviceInfo[]]} Array containing audio and video devices
*
* Each device object contains:
* - deviceId: Unique identifier for the device
* - label: Human-readable name (e.g., "Built-in Camera", "External Microphone")
* - kind: Type of device ("audioinput" or "videoinput")
*/
export function getDevices() {
const audioDevices = mediaController.audioDevices();
const videoDevices = mediaController.videoDevices();
return [audioDevices, videoDevices];
}
// << end-get-devices
// << set-initial-devices
/**
* Configure Initial Devices
*
* Sets up the encoder with the first available camera and microphone, and
* populates the device selection dropdowns with all available options.
*
* This function:
* 1. Selects the first available audio/video device
* 2. Populates dropdown menus with all available devices
* 3. Sets initial button text based on device state
*
* @param {MediaStreamController} mediaStreamController - Controller managing the media streams
* @param {MediaDeviceInfo[]} audioDevices - Array of available audio input devices
* @param {MediaDeviceInfo[]} videoDevices - Array of available video input devices
*/
export function setInitialDevices(mediaStreamController, audioDevices, videoDevices) {
const audioDeviceSelect = document.getElementById("audioDeviceSelect");
const videoDeviceSelect = document.getElementById("videoDeviceSelect");
// Configure audio devices
if (audioDevices.length > 0) {
// Set the first audio device as the active microphone
mediaStreamController.audioDeviceId = audioDevices[0].deviceId;
// Populate the audio device dropdown with all available microphones
audioDevices.forEach((item) => {
audioDeviceSelect.options[audioDeviceSelect.options.length] = new Option(item.label, item.deviceId);
});
}
// Configure video devices
if (mediaController.videoDevices().length > 0) {
// Set the first video device as the active camera
mediaStreamController.videoDeviceId = videoDevices[0].deviceId;
// Populate the video device dropdown with all available cameras
videoDevices.forEach((item) => {
videoDeviceSelect.options[videoDeviceSelect.options.length] = new Option(item.label, item.deviceId);
});
}
// Set initial button text based on current device state
document.getElementById("cameraBtn").textContent = mediaStreamController.videoPaused
? "Enable Camera"
: "Disable Camera";
document.getElementById("micBtn").textContent = mediaStreamController.audioMuted ? "Enable Mic" : "Disable Mic";
}
// << end-set-initial-devices
// << attach-event-handlers
/**
* Attach Event Handlers to Encoder Controls
*
* Sets up click and change event handlers for all encoder UI controls.
* This enables users to:
* - Toggle camera on/off
* - Toggle microphone on/off
* - Switch between available cameras
* - Switch between available microphones
*
* @param {MediaStreamController} mediaStreamController - Controller to manipulate
*/
function attachEncoderEventHandlers(mediaStreamController) {
/**
* Toggle Camera On/Off
*
* Pauses or resumes the video track. When paused, the camera is disabled
* and the video feed stops. The camera remains allocated to prevent
* other applications from accessing it.
*
* @param {Event} event - Click event from the camera button
*/
function toggleCamera(event) {
// Toggle the videoPaused state
mediaStreamController.videoPaused = !mediaStreamController.videoPaused;
// Update button text to reflect new state
event.target.textContent = mediaStreamController.videoPaused ? "Enable Camera" : "Disable Camera";
}
/**
* Toggle Microphone On/Off
*
* Mutes or unmutes the audio track. When muted, the microphone continues
* to capture audio but it won't be included in the broadcast.
*
* @param {Event} event - Click event from the microphone button
*/
function toggleMic(event) {
// Toggle the audioMuted state
mediaStreamController.audioMuted = !mediaStreamController.audioMuted;
// Update button text to reflect new state
event.target.textContent = mediaStreamController.audioMuted ? "Enable Mic" : "Disable Mic";
}
/**
* Handle Video Device Selection
*
* Switches to a different camera when the user selects one from the dropdown.
* The mediaStreamController automatically handles stopping the old device
* and starting the new one.
*
* @param {Event} ev - Change event from the video device select dropdown
*/
function handleVideoDeviceSelect(ev) {
// Update the active video device
// This triggers the controller to switch cameras
mediaStreamController.videoDeviceId = ev.target.value;
}
/**
* Handle Audio Device Selection
*
* Switches to a different microphone when the user selects one from the dropdown.
* The mediaStreamController automatically handles stopping the old device
* and starting the new one.
*
* @param {Event} ev - Change event from the audio device select dropdown
*/
function handleAudioDeviceSelect(ev) {
// Update the active audio device
// This triggers the controller to switch microphones
mediaStreamController.audioDeviceId = ev.target.value;
}
/**
* Wire Up Event Listeners
*
* Connect the handler functions to the corresponding DOM elements
*/
document.getElementById("cameraBtn").onclick = toggleCamera;
document.getElementById("micBtn").onclick = toggleMic;
document.getElementById("videoDeviceSelect").onchange = handleVideoDeviceSelect;
document.getElementById("audioDeviceSelect").onchange = handleAudioDeviceSelect;
}
// << end-attach-event-handlers
Main Application Logic
The main application file orchestrates authentication, encoder setup, calls, and broadcasts.
/**
* Native Frame Encoder - Main Application
*
* This is the main application file for the livestream encoder. It orchestrates:
* - Authentication setup
* - Encoder initialization (camera/microphone access and preview)
* - Call creation and management
* - Broadcast start/stop control
* - Resource cleanup
*
* Flow:
* 1. Page loads → init() is called
* 2. Authentication client is created
* 3. Encoder (media devices + preview) is initialized
* 4. User clicks "Start Call" → Call is created
* 5. User clicks "Start Broadcast" → Broadcasting begins
* 6. User can stop broadcast and/or call at any time
* 7. Resources are cleaned up on page unload
*/
// << encoder-imports
import { setAuthClient, backendEndpoint, streamKey, broadcasterToken } from './auth.js';
import { requestEncoder } from './encoder-utils.js';
import { createCall } from 'vdc-cdn';
// << end-encoder-imports
// Internal testing utility - not part of the public API
import { initViewerPageButton } from './qa-buttons.js';
// << state-variables
/**
* Application State Variables
*
* These module-level variables maintain the state of the encoder application:
*
* - mediaStreamController: Manages camera and microphone access and control
* - previewPlayer: Displays the local video preview before/during broadcast
* - authClient: Authenticated client for API requests
* - call: Represents the connection to the Native Frame backend
* - broadcast: Represents the active broadcast stream
*
* All are initialized to null and populated during the init() process.
*/
let mediaStreamController = null;
let previewPlayer = null;
let authClient = null;
let call = null;
let broadcast = null;
// << end-state-variables
// << toggle-broadcast
/**
* Toggle Broadcast On/Off
*
* Starts or stops broadcasting the media stream. Broadcasting sends your
* audio/video to viewers who are watching the stream.
*
* Requirements:
* - A call must be active before broadcasting can start
* - The mediaStreamController must have active camera/microphone
*
* @param {Event} event - Click event from the broadcast button
*
* The streamName parameter identifies which stream this is:
* - "default": Main camera/microphone feed
* - Other names can be used for additional streams (e.g., screen sharing)
*/
async function toggleBroadcast(event) {
if (broadcast == null) {
// Start broadcasting
// This sends the media stream to the Native Frame backend for distribution to viewers
broadcast = await call.broadcast(mediaStreamController, { streamName: "default" });
// Update button to show "Stop" state
event.target.textContent = "Stop Broadcast";
event.target.setAttribute("data-call-id", call.id);
document.getElementById("broadcastBtn").style.display = "block";
} else {
// Stop broadcasting
// This stops sending the stream but keeps the call active
broadcast.dispose("broadcast disposed via toggleBroadcast()");
broadcast = null;
// Update button to show "Start" state
event.target.textContent = "Start Broadcast";
document.getElementById("broadcastBtn").style.display = "none";
}
}
// << end-toggle-broadcast
// << toggle-call
/**
* Toggle Call Connection On/Off
*
* Creates or terminates the connection to the Native Frame backend.
* A call must be active before you can start broadcasting.
*
* Call Options:
* - user: Identifies the broadcaster (userId and displayName)
* - streamKey: Unique identifier for this stream (from Native Frame)
* - backendEndpoints: Array of backend URLs to try connecting to
* - auth: Authenticated client for API requests
*
* @param {Event} event - Click event from the call button
*/
async function toggleCall(event) {
if (call == null) {
// Create a new call
const callOptions = {
// User information for this broadcaster
user: { userId: "123", displayName: "John Doe" },
// Stream identifier (from authentication configuration)
streamKey,
// Backend endpoints to connect to (with automatic failover)
backendEndpoints: [backendEndpoint],
// Authentication client
auth: authClient,
};
// Create the call - this establishes the connection to the backend
call = await createCall(callOptions);
// Update UI to show call is active
event.target.textContent = "Stop Call";
document.getElementById("broadcastBtn").textContent = "Start Broadcast";
document.getElementById("broadcastBtn").style.display = "block";
} else {
// Terminate the call
// This also stops any active broadcast
call.dispose("call disposed via callState toggleCall()");
call = null;
// Update UI to show call is inactive
event.target.textContent = "Start Call";
document.getElementById("broadcastBtn").style.display = "none";
}
}
// << end-toggle-call
// << init-function
/**
* Initialize the Encoder Application
*
* This is the main initialization function that sets up the entire encoder.
* It runs when the page loads and performs these steps:
*
* 1. Creates an authenticated client using the broadcaster token
* 2. Requests camera/microphone access and creates the encoder UI
* 3. Sets up event handlers for call and broadcast buttons
* 4. Shows the "Start Call" button to begin
* 5. Registers cleanup handlers for page unload
*
* After init() completes, the user can click "Start Call" to connect,
* then "Start Broadcast" to begin streaming.
*/
async function init() {
// Create authenticated client for API requests
authClient = await setAuthClient(broadcasterToken);
// Initialize encoder (camera/mic access, preview player, and UI)
const [msc, preview] = await requestEncoder();
mediaStreamController = msc;
previewPlayer = preview;
// Wire up button click handlers
document.getElementById("callBtn").onclick = toggleCall;
document.getElementById("broadcastBtn").onclick = toggleBroadcast;
// Show the "Start Call" button
document.getElementById("callBtn").style.display = "block";
// Set up cleanup on page unload
disposeOnBeforeUnload();
}
// << end-init-function
// << cleanup
/**
* Clean Up Resources
*
* Disposes of all active resources to prevent memory leaks and release
* camera/microphone access. This should be called when:
* - The page is being unloaded
* - The user navigates away
* - The application is shutting down
*
* Important: Always dispose of video-client resources when done to:
* - Release camera/microphone so other apps can use them
* - Close network connections
* - Free up memory
*/
function dispose() {
if (document.hidden) {
// Dispose of media devices (releases camera/microphone)
mediaStreamController?.dispose();
// Dispose of the preview player (releases video element)
previewPlayer?.dispose();
// Dispose of the call (closes network connection)
call?.dispose();
// Clear all references
call = null;
mediaStreamController = null;
previewPlayer = null;
authClient = null;
}
}
/**
* Register Cleanup Handler
*
* Sets up an event listener to clean up resources when the page unloads.
* This ensures camera/microphone access is properly released.
*/
function disposeOnBeforeUnload() {
window.addEventListener("beforeunload", () => {
dispose();
});
}
// << end-cleanup
// << window-onload
/**
* Application Entry Point
*
* This runs when the page finishes loading. It calls init() to set up
* the encoder and make it ready for use.
*/
window.onload = async () => {
await init();
// Internal testing utility - ignore for implementation
initViewerPageButton();
};
// << end-window-onload
Full Code
<!--
Native Frame Preview Player - Vanilla JavaScript Implementation
This HTML file demonstrates how to set up a livestream preview player using the
@video/video-client-core library with vanilla JavaScript (no framework required).
The preview player allows a broadcaster to:
- Access camera and microphone
- Preview their video feed
-->
<!doctype html>
<html>
<head>
<!-- << importmap -->
<!--
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>
<!-- << end-importmap -->
<!-- << imports -->
<!--
JavaScript Module Imports
These modules must be loaded in order:
1. auth.js - Handles authentication with the Native Frame backend
2. encoder-utils.js - Provides helper functions for setting up the encoder UI
3. encoder.js - Main application logic for managing calls and broadcasts
-->
<script type="module" src="/js/auth.js"></script>
<script type="module" src="/js/encoder-utils.js"></script>
<script type="module" src="/js/encoder.js"></script>
<!-- << end-imports -->
</head>
<body>
<!--
Encoder UI Structure
The page consists of three main elements:
1. encoderContainer - Will be populated with the video preview and device controls
2. callBtn - Button to create/end the call connection
3. broadcastBtn - Button to start/stop broadcasting the stream
These elements are manipulated by the JavaScript modules to show/hide controls
based on the current state of the encoder.
-->
<div>
<!-- Video preview and device controls will be injected here -->
<div id="encoderContainer"></div>
<!-- Initially hidden, shown after encoder initialization -->
<button id="callBtn" style="display: none;">Start Call</button>
<!-- Initially hidden, shown after call is created -->
<button id="broadcastBtn" style="display: none;"></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 = await new BaseAuthClient(token);
return auth;
}
/**
* Encoder Utilities Module
*
* This module provides utility functions for setting up the video encoder UI.
* It handles:
* - Requesting access to camera and microphone
* - Creating the media stream controller
* - Setting up the preview player
* - Creating and managing the encoder UI elements
* - Attaching event handlers for device controls
*
* Key Concepts:
* - MediaStreamController: Manages access to camera/microphone and controls their state
* - PreviewPlayer: Displays the local video feed before broadcasting
* - MediaController: Global singleton that manages device enumeration and permissions
*/
import { requestPlayer, mediaController } from 'vdc-cdn'
// << request-encoder
/**
* Initialize Encoder with Media Devices
*
* This is the main initialization function for the encoder. It:
* 1. Requests camera and microphone permissions
* 2. Creates a MediaStreamController to manage the devices
* 3. Creates a PreviewPlayer to display the video
* 4. Builds the UI for device controls
* 5. Attaches event handlers for user interactions
*
* @returns {Promise<[MediaStreamController, PreviewPlayer]>} Array containing the controller and player
*
* Usage:
* const [mediaStreamController, previewPlayer] = await requestEncoder();
*
* The returned objects are used throughout the application:
* - mediaStreamController: Used to start/stop camera/mic and switch devices
* - previewPlayer: Used to display the video feed and must be disposed when done
*/
export async function requestEncoder() {
// Initialize the global media controller
// This requests browser permissions for camera and microphone access
await mediaController.init();
// Create a MediaStreamController instance
// This manages the actual media streams from the selected devices
const mediaStreamController = await mediaController.requestController();
// Enumerate available audio and video devices
// Returns arrays of available cameras and microphones
const [audioDevices, videoDevices] = getDevices();
// Create a preview player to display the local video feed
// autoPlay: true - automatically starts playing when attached
// muted: true - mutes the local preview (prevents audio feedback)
const previewPlayer = await requestPlayer(mediaStreamController, { autoPlay: true, muted: true });
// Create the video element and append it to the DOM
// Returns a reference to the video element
const video = appendEncoderHTMLToDOM();
// Attach the preview player to the video element
// This connects the media stream to the video tag for display
previewPlayer.attachTo(video);
// Set up click handlers for camera/mic toggle buttons and device selection
attachEncoderEventHandlers(mediaStreamController);
// Configure the initial devices (first available camera/mic)
// and populate the device selection dropdowns
setInitialDevices(mediaStreamController, audioDevices, videoDevices);
// Return both objects for use in the main application
return [mediaStreamController, previewPlayer];
}
// << end-request-encoder
// << create-encoder-html
/**
* Generate HTML for Encoder UI
*
* Creates the HTML structure for the video preview and device controls.
* This includes:
* - Video element for displaying the preview
* - Camera toggle button
* - Microphone toggle button
* - Video device selector dropdown
* - Audio device selector dropdown
*
* @returns {string} HTML string containing the encoder UI structure
*
* The generated HTML will be injected into the #encoderContainer div.
*/
export function createEncoderHTML() {
return `
<div class="video-wrapper">
<!-- Video element where the preview will be displayed -->
<video id="preview-player" style="height: 100%; width: 100%"></video>
<div class="controls">
<!-- Toggle camera on/off -->
<button id="cameraBtn"></button>
<!-- Toggle microphone on/off -->
<button id="micBtn"></button>
<!-- Select video input device (camera) -->
<label for="videoDeviceSelect">
Choose video device:
<select id="videoDeviceSelect" name="videoDeviceSelect"></select>
</label>
<!-- Select audio input device (microphone) -->
<label for="audioDeviceSelect">
Choose audio device:
<select id="audioDeviceSelect" name="audioDeviceSelect"></select>
</label>
</div>
</div>
`;
}
// << end-create-encoder-html
// << append-encoder-html
/**
* Append Encoder HTML to DOM
*
* Injects the encoder UI into the page and returns a reference to the video element.
* This function:
* 1. Finds the encoder container div
* 2. Inserts the encoder HTML
* 3. Returns the video element for player attachment
*
* @returns {HTMLVideoElement} Reference to the video element
*
* The video element is returned so the PreviewPlayer can be attached to it.
*/
function appendEncoderHTMLToDOM() {
// Find the container where we'll inject the encoder UI
const encoderContainer = document.getElementById("encoderContainer");
// Insert the encoder HTML into the container
encoderContainer.insertAdjacentHTML("beforeend", createEncoderHTML());
// Return reference to the video element
return document.getElementById("preview-player");
}
// << end-append-encoder-html
// << get-devices
/**
* Get Available Media Devices
*
* Retrieves lists of available audio and video input devices from the mediaController.
* This includes all cameras and microphones that the user has granted permission to access.
*
* @returns {[MediaDeviceInfo[], MediaDeviceInfo[]]} Array containing audio and video devices
*
* Each device object contains:
* - deviceId: Unique identifier for the device
* - label: Human-readable name (e.g., "Built-in Camera", "External Microphone")
* - kind: Type of device ("audioinput" or "videoinput")
*/
export function getDevices() {
const audioDevices = mediaController.audioDevices();
const videoDevices = mediaController.videoDevices();
return [audioDevices, videoDevices];
}
// << end-get-devices
// << set-initial-devices
/**
* Configure Initial Devices
*
* Sets up the encoder with the first available camera and microphone, and
* populates the device selection dropdowns with all available options.
*
* This function:
* 1. Selects the first available audio/video device
* 2. Populates dropdown menus with all available devices
* 3. Sets initial button text based on device state
*
* @param {MediaStreamController} mediaStreamController - Controller managing the media streams
* @param {MediaDeviceInfo[]} audioDevices - Array of available audio input devices
* @param {MediaDeviceInfo[]} videoDevices - Array of available video input devices
*/
export function setInitialDevices(mediaStreamController, audioDevices, videoDevices) {
const audioDeviceSelect = document.getElementById("audioDeviceSelect");
const videoDeviceSelect = document.getElementById("videoDeviceSelect");
// Configure audio devices
if (audioDevices.length > 0) {
// Set the first audio device as the active microphone
mediaStreamController.audioDeviceId = audioDevices[0].deviceId;
// Populate the audio device dropdown with all available microphones
audioDevices.forEach((item) => {
audioDeviceSelect.options[audioDeviceSelect.options.length] = new Option(item.label, item.deviceId);
});
}
// Configure video devices
if (mediaController.videoDevices().length > 0) {
// Set the first video device as the active camera
mediaStreamController.videoDeviceId = videoDevices[0].deviceId;
// Populate the video device dropdown with all available cameras
videoDevices.forEach((item) => {
videoDeviceSelect.options[videoDeviceSelect.options.length] = new Option(item.label, item.deviceId);
});
}
// Set initial button text based on current device state
document.getElementById("cameraBtn").textContent = mediaStreamController.videoPaused
? "Enable Camera"
: "Disable Camera";
document.getElementById("micBtn").textContent = mediaStreamController.audioMuted ? "Enable Mic" : "Disable Mic";
}
// << end-set-initial-devices
// << attach-event-handlers
/**
* Attach Event Handlers to Encoder Controls
*
* Sets up click and change event handlers for all encoder UI controls.
* This enables users to:
* - Toggle camera on/off
* - Toggle microphone on/off
* - Switch between available cameras
* - Switch between available microphones
*
* @param {MediaStreamController} mediaStreamController - Controller to manipulate
*/
function attachEncoderEventHandlers(mediaStreamController) {
/**
* Toggle Camera On/Off
*
* Pauses or resumes the video track. When paused, the camera is disabled
* and the video feed stops. The camera remains allocated to prevent
* other applications from accessing it.
*
* @param {Event} event - Click event from the camera button
*/
function toggleCamera(event) {
// Toggle the videoPaused state
mediaStreamController.videoPaused = !mediaStreamController.videoPaused;
// Update button text to reflect new state
event.target.textContent = mediaStreamController.videoPaused ? "Enable Camera" : "Disable Camera";
}
/**
* Toggle Microphone On/Off
*
* Mutes or unmutes the audio track. When muted, the microphone continues
* to capture audio but it won't be included in the broadcast.
*
* @param {Event} event - Click event from the microphone button
*/
function toggleMic(event) {
// Toggle the audioMuted state
mediaStreamController.audioMuted = !mediaStreamController.audioMuted;
// Update button text to reflect new state
event.target.textContent = mediaStreamController.audioMuted ? "Enable Mic" : "Disable Mic";
}
/**
* Handle Video Device Selection
*
* Switches to a different camera when the user selects one from the dropdown.
* The mediaStreamController automatically handles stopping the old device
* and starting the new one.
*
* @param {Event} ev - Change event from the video device select dropdown
*/
function handleVideoDeviceSelect(ev) {
// Update the active video device
// This triggers the controller to switch cameras
mediaStreamController.videoDeviceId = ev.target.value;
}
/**
* Handle Audio Device Selection
*
* Switches to a different microphone when the user selects one from the dropdown.
* The mediaStreamController automatically handles stopping the old device
* and starting the new one.
*
* @param {Event} ev - Change event from the audio device select dropdown
*/
function handleAudioDeviceSelect(ev) {
// Update the active audio device
// This triggers the controller to switch microphones
mediaStreamController.audioDeviceId = ev.target.value;
}
/**
* Wire Up Event Listeners
*
* Connect the handler functions to the corresponding DOM elements
*/
document.getElementById("cameraBtn").onclick = toggleCamera;
document.getElementById("micBtn").onclick = toggleMic;
document.getElementById("videoDeviceSelect").onchange = handleVideoDeviceSelect;
document.getElementById("audioDeviceSelect").onchange = handleAudioDeviceSelect;
}
// << end-attach-event-handlers
/**
* Native Frame Encoder - Main Application
*
* This is the main application file for the livestream encoder. It orchestrates:
* - Authentication setup
* - Encoder initialization (camera/microphone access and preview)
* - Call creation and management
* - Broadcast start/stop control
* - Resource cleanup
*
* Flow:
* 1. Page loads → init() is called
* 2. Authentication client is created
* 3. Encoder (media devices + preview) is initialized
* 4. User clicks "Start Call" → Call is created
* 5. User clicks "Start Broadcast" → Broadcasting begins
* 6. User can stop broadcast and/or call at any time
* 7. Resources are cleaned up on page unload
*/
// << encoder-imports
import { setAuthClient, backendEndpoint, streamKey, broadcasterToken } from './auth.js';
import { requestEncoder } from './encoder-utils.js';
import { createCall } from 'vdc-cdn';
// << end-encoder-imports
// Internal testing utility - not part of the public API
import { initViewerPageButton } from './qa-buttons.js';
// << state-variables
/**
* Application State Variables
*
* These module-level variables maintain the state of the encoder application:
*
* - mediaStreamController: Manages camera and microphone access and control
* - previewPlayer: Displays the local video preview before/during broadcast
* - authClient: Authenticated client for API requests
* - call: Represents the connection to the Native Frame backend
* - broadcast: Represents the active broadcast stream
*
* All are initialized to null and populated during the init() process.
*/
let mediaStreamController = null;
let previewPlayer = null;
let authClient = null;
let call = null;
let broadcast = null;
// << end-state-variables
// << toggle-broadcast
/**
* Toggle Broadcast On/Off
*
* Starts or stops broadcasting the media stream. Broadcasting sends your
* audio/video to viewers who are watching the stream.
*
* Requirements:
* - A call must be active before broadcasting can start
* - The mediaStreamController must have active camera/microphone
*
* @param {Event} event - Click event from the broadcast button
*
* The streamName parameter identifies which stream this is:
* - "default": Main camera/microphone feed
* - Other names can be used for additional streams (e.g., screen sharing)
*/
async function toggleBroadcast(event) {
if (broadcast == null) {
// Start broadcasting
// This sends the media stream to the Native Frame backend for distribution to viewers
broadcast = await call.broadcast(mediaStreamController, { streamName: "default" });
// Update button to show "Stop" state
event.target.textContent = "Stop Broadcast";
event.target.setAttribute("data-call-id", call.id);
document.getElementById("broadcastBtn").style.display = "block";
} else {
// Stop broadcasting
// This stops sending the stream but keeps the call active
broadcast.dispose("broadcast disposed via toggleBroadcast()");
broadcast = null;
// Update button to show "Start" state
event.target.textContent = "Start Broadcast";
document.getElementById("broadcastBtn").style.display = "none";
}
}
// << end-toggle-broadcast
// << toggle-call
/**
* Toggle Call Connection On/Off
*
* Creates or terminates the connection to the Native Frame backend.
* A call must be active before you can start broadcasting.
*
* Call Options:
* - user: Identifies the broadcaster (userId and displayName)
* - streamKey: Unique identifier for this stream (from Native Frame)
* - backendEndpoints: Array of backend URLs to try connecting to
* - auth: Authenticated client for API requests
*
* @param {Event} event - Click event from the call button
*/
async function toggleCall(event) {
if (call == null) {
// Create a new call
const callOptions = {
// User information for this broadcaster
user: { userId: "123", displayName: "John Doe" },
// Stream identifier (from authentication configuration)
streamKey,
// Backend endpoints to connect to (with automatic failover)
backendEndpoints: [backendEndpoint],
// Authentication client
auth: authClient,
};
// Create the call - this establishes the connection to the backend
call = await createCall(callOptions);
// Update UI to show call is active
event.target.textContent = "Stop Call";
document.getElementById("broadcastBtn").textContent = "Start Broadcast";
document.getElementById("broadcastBtn").style.display = "block";
} else {
// Terminate the call
// This also stops any active broadcast
call.dispose("call disposed via callState toggleCall()");
call = null;
// Update UI to show call is inactive
event.target.textContent = "Start Call";
document.getElementById("broadcastBtn").style.display = "none";
}
}
// << end-toggle-call
// << init-function
/**
* Initialize the Encoder Application
*
* This is the main initialization function that sets up the entire encoder.
* It runs when the page loads and performs these steps:
*
* 1. Creates an authenticated client using the broadcaster token
* 2. Requests camera/microphone access and creates the encoder UI
* 3. Sets up event handlers for call and broadcast buttons
* 4. Shows the "Start Call" button to begin
* 5. Registers cleanup handlers for page unload
*
* After init() completes, the user can click "Start Call" to connect,
* then "Start Broadcast" to begin streaming.
*/
async function init() {
// Create authenticated client for API requests
authClient = await setAuthClient(broadcasterToken);
// Initialize encoder (camera/mic access, preview player, and UI)
const [msc, preview] = await requestEncoder();
mediaStreamController = msc;
previewPlayer = preview;
// Wire up button click handlers
document.getElementById("callBtn").onclick = toggleCall;
document.getElementById("broadcastBtn").onclick = toggleBroadcast;
// Show the "Start Call" button
document.getElementById("callBtn").style.display = "block";
// Set up cleanup on page unload
disposeOnBeforeUnload();
}
// << end-init-function
// << cleanup
/**
* Clean Up Resources
*
* Disposes of all active resources to prevent memory leaks and release
* camera/microphone access. This should be called when:
* - The page is being unloaded
* - The user navigates away
* - The application is shutting down
*
* Important: Always dispose of video-client resources when done to:
* - Release camera/microphone so other apps can use them
* - Close network connections
* - Free up memory
*/
function dispose() {
if (document.hidden) {
// Dispose of media devices (releases camera/microphone)
mediaStreamController?.dispose();
// Dispose of the preview player (releases video element)
previewPlayer?.dispose();
// Dispose of the call (closes network connection)
call?.dispose();
// Clear all references
call = null;
mediaStreamController = null;
previewPlayer = null;
authClient = null;
}
}
/**
* Register Cleanup Handler
*
* Sets up an event listener to clean up resources when the page unloads.
* This ensures camera/microphone access is properly released.
*/
function disposeOnBeforeUnload() {
window.addEventListener("beforeunload", () => {
dispose();
});
}
// << end-cleanup
// << window-onload
/**
* Application Entry Point
*
* This runs when the page finishes loading. It calls init() to set up
* the encoder and make it ready for use.
*/
window.onload = async () => {
await init();
// Internal testing utility - ignore for implementation
initViewerPageButton();
};
// << end-window-onload
Next Steps
To learn more about the video-client-core library and advanced features:
- Learn about customizing your streaming app
- Set up a screenshare
- Build out components to view a livestream