Skip to content

Video Player Integration

Build a custom web video player for Rhombus camera streams with HLS live playback, recorded video VOD, scrubbing, and timeline controls using the API.

On this page
Info

Full Example Available

The complete working implementation is available on GitHub: RhombusSystems/player-example

Overview

The Rhombus video player integration enables you to:

Stream Live Video

Display real-time camera feeds with adaptive bitrate streaming

Secure Authentication

Use federated session tokens for time-limited, secure access

Optimized Playback

Pre-configured settings optimized for security camera footage

Drop-in integration

Drop-in solution with minimal configuration required

Architecture

The video player implementation follows a three-tier architecture:

Loading diagram…
  1. Frontend Requests Token

    Your web application requests authentication from your backend server

  2. Backend Proxies to Rhombus

    Your server authenticates with Rhombus API using your API key and returns a federated token

  3. Frontend Gets Media URI

    Your frontend asks your backend for the camera's streaming URI; your backend calls getMediaUris with the API key

  4. Direct Stream Playback

    DashJS connects directly to Rhombus and adds the token to every request: the manifest and each segment

Prerequisites

Before implementing the video player, ensure you have:

Rhombus API Credentials

Generate an API key from your Rhombus Console. This key will be used by your backend server to authenticate with the Rhombus API.

Warning

Never expose your API key in frontend code. Always use a backend proxy server.

Camera UUID

Obtain your camera's UUID from the Rhombus Console or via the API:

bash
curl -X POST "https://api2.rhombussystems.com/api/camera/getMinimalCameraStateList" \
  -H "x-auth-apikey: YOUR_API_KEY" \
  -H "x-auth-scheme: api-token"
Backend Server

Set up a proxy server to handle API authentication. Your server must implement two endpoints:

  • Token generation endpoint (forwards to /org/generateFederatedSessionToken)
  • Media URI endpoint (forwards to /camera/getMediaUris)

Implementation guide

Step 1: Set up your HTML page

Create a basic HTML structure with the DashJS player:

index.htmlhtml
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Rhombus Camera Stream</title>
    <script src="https://cdn.dashjs.org/latest/dash.all.min.js"></script>
    <style>
        body {
            margin: 0;
            padding: 20px;
            font-family: Arial, sans-serif;
            background-color: #1a1a1a;
        }
        #video-container {
            max-width: 1280px;
            margin: 0 auto;
        }
        video {
            width: 100%;
            background-color: #000;
            border-radius: 8px;
        }
        .controls {
            margin-top: 20px;
            text-align: center;
        }
        button {
            padding: 10px 20px;
            margin: 0 5px;
            background-color: #2563EB;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
        }
        button:hover {
            background-color: #1D4ED8;
        }
    </style>
</head>
<body>
    <div id="video-container">
        <video id="videoPlayer" controls></video>
        <div class="controls">
            <button onclick="play()">Play</button>
            <button onclick="pause()">Pause</button>
        </div>
    </div>

    <script src="player.js"></script>
</body>
</html>

Step 2: Configure player settings

Create your player configuration file:

player.jsjavascript
// Configuration - Replace with your values
const CAMERA_UUID = "YOUR_CAMERA_UUID_HERE";
// Point BASE_URL at YOUR backend proxy server — never at api2.rhombussystems.com
// directly. The API key stays on your server; the browser only talks to your proxy.
const BASE_URL = "http://localhost:3000";       // your backend server
const GET_FEDERATED_TOKEN_PATH = "/api/federated-token";  // your proxy route
const GET_MEDIA_URIS_PATH = "/api/media-uris";            // your proxy route

// Global variables
let player;
let federatedToken;

// Initialize player on page load
document.addEventListener('DOMContentLoaded', async () => {
    await initializePlayer();
});

Step 3: Implement authentication

Add token management functionality:

Token Managementjavascript
async function getFederatedSessionToken(cameraUuid = CAMERA_UUID) {
    try {
        const response = await fetch(`${BASE_URL}${GET_FEDERATED_TOKEN_PATH}`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                cameraUuid  // your backend mints a short-lived token for this camera only
            })
        });

        if (!response.ok) {
            throw new Error(`Token request failed: ${response.status}`);
        }

        const data = await response.json();
        federatedToken = data.federatedSessionToken;

        console.log('Federated token obtained successfully');
        return federatedToken;
    } catch (error) {
        console.error('Error getting federated token:', error);
        throw error;
    }
}

// Add the token to every stream request: the manifest and each segment
function withAuth(url) {
    if (url.includes('x-auth-ft=')) return url;
    const auth = `x-auth-scheme=federated-token&x-auth-ft=${encodeURIComponent(federatedToken)}`;
    return `${url}${url.includes('?') ? '&' : '?'}${auth}`;
}

Step 4: Get media stream URI

Implement media URI retrieval:

Media URI Requestjavascript
async function getMediaUri() {
    try {
        const response = await fetch(`${BASE_URL}${GET_MEDIA_URIS_PATH}`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                cameraUuid: CAMERA_UUID
            })
        });

        if (!response.ok) {
            throw new Error(`Media URI request failed: ${response.status}`);
        }

        const data = await response.json();
        return data.wanLiveMpdUri;  // DASH manifest URL
    } catch (error) {
        console.error('Error getting media URI:', error);
        throw error;
    }
}

Step 5: Initialize DashJS player

Set up the video player with optimized settings:

Player Initializationjavascript
async function initializePlayer() {
    try {
        // Get authentication token
        await getFederatedSessionToken();

        // Get media stream URL
        const mediaUrl = await getMediaUri();

        // Initialize DashJS player
        const video = document.querySelector("#videoPlayer");
        player = dashjs.MediaPlayer().create();

        // Configure player settings optimized for security cameras
        player.updateSettings({
            streaming: {
                liveCatchup: {
                    enabled: true,
                    mode: 'liveCatchupModeDefault',
                    maxDrift: 10,
                    playbackRate: {
                        min: -0.5,
                        max: 0.5
                    }
                },
                buffer: {
                    fastSwitchEnabled: false,
                    stableBufferTime: 12,
                    bufferTimeAtTopQuality: 30,
                    bufferTimeAtTopQualityLongForm: 60
                },
                gaps: {
                    jumpGaps: true,
                    jumpLargeGaps: true,
                    smallGapLimit: 1.5
                },
                stallThreshold: 0.5,
                scheduleWhilePaused: false
            }
        });

        // Add the token to every request dash.js makes, the manifest included
        player.addRequestInterceptor((request) => {
            request.url = withAuth(request.url);
            return Promise.resolve(request);
        });

        // Initialize and play
        player.initialize(video, mediaUrl, true);

        console.log('Player initialized successfully');
    } catch (error) {
        console.error('Error initializing player:', error);
        alert('Failed to initialize video player. Check console for details.');
    }
}

DASH segment URLs don't carry the manifest's query string, so the interceptor adds the token to the manifest request and to every segment. addRequestInterceptor is the dash.js 5 API, which cdn.dashjs.org/latest serves. See Media and streams.

Step 6: Add playback controls

Implement play/pause functionality:

Playback Controlsjavascript
function play() {
    if (player) {
        player.play();
    }
}

function pause() {
    if (player) {
        player.pause();
    }
}

// Optional: Handle player events
function setupPlayerEvents() {
    player.on(dashjs.MediaPlayer.events.PLAYBACK_STARTED, () => {
        console.log('Playback started');
    });

    player.on(dashjs.MediaPlayer.events.PLAYBACK_PAUSED, () => {
        console.log('Playback paused');
    });

    player.on(dashjs.MediaPlayer.events.ERROR, (e) => {
        console.error('Player error:', e);
    });
}

Backend server implementation

Your backend server must proxy requests to the Rhombus API. Here's an example using Node.js/Express:

server.jsjavascript
const express = require('express');
const axios = require('axios');
const app = express();

app.use(express.json());

const RHOMBUS_API_KEY = process.env.RHOMBUS_API_KEY;
const RHOMBUS_BASE_URL = 'https://api2.rhombussystems.com/api';

// Federated token endpoint (the browser's GET_FEDERATED_TOKEN_PATH)
app.post('/api/federated-token', async (req, res) => {
    try {
        const response = await axios.post(
            `${RHOMBUS_BASE_URL}/org/generateFederatedSessionToken`,
            {
                durationSec: 3600,               // 1 hour; there is no default
                deviceUUid: req.body.cameraUuid  // only this camera's media
            },
            {
                headers: {
                    'x-auth-apikey': RHOMBUS_API_KEY,
                    'x-auth-scheme': 'api-token',
                    'Content-Type': 'application/json'
                }
            }
        );
        res.json(response.data);
    } catch (error) {
        console.error('Error generating token:', error);
        res.status(500).json({ error: 'Failed to generate token' });
    }
});

// Media URI endpoint (the browser's GET_MEDIA_URIS_PATH)
app.post('/api/media-uris', async (req, res) => {
    try {
        const response = await axios.post(
            `${RHOMBUS_BASE_URL}/camera/getMediaUris`,
            { cameraUuid: req.body.cameraUuid },
            {
                headers: {
                    'x-auth-apikey': RHOMBUS_API_KEY,
                    'x-auth-scheme': 'api-token',
                    'Content-Type': 'application/json'
                }
            }
        );
        res.json(response.data);
    } catch (error) {
        console.error('Error getting media URIs:', error);
        res.status(500).json({ error: 'Failed to get media URIs' });
    }
});

app.listen(3000, () => {
    console.log('Server running on port 3000');
});

Player configuration options

The DashJS player can be customized with various settings optimized for different use cases:

liveCatchup.enabledbooleanpathdefault: true

Enable automatic catch-up to live edge when playback falls behind

liveCatchup.maxDriftnumberpathdefault: 10

Maximum allowed drift from live edge in seconds before catch-up kicks in

buffer.stableBufferTimenumberpathdefault: 12

Target buffer size for stable playback in seconds

buffer.fastSwitchEnabledbooleanpathdefault: false

Disable fast quality switching to maintain consistent video quality

gaps.jumpGapsbooleanpathdefault: true

Automatically skip small gaps in the stream

stallThresholdnumberpathdefault: 0.5

Time in seconds before considering playback stalled

Security best practices

Warning

Critical Security Requirements

Follow these security practices to protect your API credentials and ensure secure streaming:

Never Expose API Keys
  • Store API keys only on your backend server
  • Use environment variables for sensitive credentials
  • Never commit API keys to version control
  • Rotate keys regularly
Implement Access Controls
  • Authenticate users before granting stream access
  • Implement rate limiting on your proxy endpoints
  • Log all access requests for audit trails
  • Use short-lived federated tokens scoped to one camera (deviceUUid), and mint a new one before it expires
Validate All Requests
  • Sanitize camera UUID inputs
  • Validate token expiration
  • Check user permissions before proxying requests
  • Implement CSRF protection
Use HTTPS Everywhere
  • Enforce HTTPS for all communications
  • Implement proper SSL/TLS certificates
  • Enable HSTS headers
  • Validate certificate chains

Troubleshooting

Video Not Loading

Check these common issues:

  1. Verify your backend server is running and accessible
  2. Confirm camera UUID is correct (check Rhombus Console)
  3. Check browser console for error messages
  4. Verify API key has proper permissions
  5. Ensure CORS is properly configured on your server

Test your endpoints:

bash
# Test token generation
curl -X POST "https://api2.rhombussystems.com/api/org/generateFederatedSessionToken" \
  -H "x-auth-scheme: api-token" \
  -H "x-auth-apikey: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"durationSec": 3600}'

# Test media URI retrieval
curl -X POST "https://api2.rhombussystems.com/api/camera/getMediaUris" \
  -H "x-auth-scheme: api-token" \
  -H "x-auth-apikey: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"cameraUuid": "YOUR_CAMERA_UUID"}'
Playback Issues

Buffering or stuttering:

  • Reduce bufferTimeAtTopQuality for faster startup
  • Increase stableBufferTime for smoother playback
  • Check network bandwidth and latency
  • Verify CDN connectivity

Stream disconnects:

  • Check token expiration: a token lasts the durationSec your backend asked for (there is no default). Mint a new one before it expires
  • Verify continuous network connectivity
  • Review firewall rules for CDN access
  • Check browser console for errors
Authentication Errors

Token generation fails:

  • Verify the API key exists and belongs to your organization's region (keys don't expire, but deleted keys stop working)
  • Check API key permissions in Rhombus Console
  • Ensure headers are correctly formatted
  • Verify account is active

Token rejected during playback:

  • Check token expiration time
  • Verify URL parameters are properly appended
  • Ensure the token is added to every request, the manifest included (see Step 5)
  • Check for special characters in token

Advanced features

Multi-camera grid

Display multiple camera feeds simultaneously:

Multi-Camera Setupjavascript
class MultiCameraPlayer {
    constructor() {
        this.players = new Map();
    }

    async addCamera(cameraUuid, containerId) {
        const token = await getFederatedSessionToken(cameraUuid);  // one token per camera
        const mediaUrl = await getMediaUri(cameraUuid);

        const video = document.querySelector(`#${containerId}`);
        const player = dashjs.MediaPlayer().create();

        player.updateSettings({
            streaming: {
                liveCatchup: { enabled: true, maxDrift: 10 },
                buffer: { stableBufferTime: 8 }
            }
        });

        player.addRequestInterceptor((request) => {
            request.url = this.withAuth(request.url, token);
            return Promise.resolve(request);
        });

        player.initialize(video, mediaUrl, true);
        this.players.set(cameraUuid, player);
    }

    withAuth(url, token) {
        if (url.includes('x-auth-ft=')) return url;
        const auth = `x-auth-scheme=federated-token&x-auth-ft=${encodeURIComponent(token)}`;
        return `${url}${url.includes('?') ? '&' : '?'}${auth}`;
    }
}

// Usage
const multiPlayer = new MultiCameraPlayer();
await multiPlayer.addCamera('camera-uuid-1', 'video1');
await multiPlayer.addCamera('camera-uuid-2', 'video2');

Custom controls

Build custom video controls:

Custom Controlsjavascript
class VideoControls {
    constructor(player) {
        this.player = player;
        this.setupEventListeners();
    }

    setupEventListeners() {
        // Volume control
        document.querySelector('#volume-slider').addEventListener('input', (e) => {
            this.player.setVolume(e.target.value / 100);
        });

        // Quality selection
        document.querySelector('#quality-select').addEventListener('change', (e) => {
            const quality = parseInt(e.target.value);
            this.player.setQualityFor('video', quality);
        });

        // Fullscreen toggle
        document.querySelector('#fullscreen-btn').addEventListener('click', () => {
            const video = document.querySelector('#videoPlayer');
            if (video.requestFullscreen) {
                video.requestFullscreen();
            }
        });
    }

    updateQualityOptions() {
        const bitrates = this.player.getBitrateInfoListFor('video');
        const select = document.querySelector('#quality-select');

        bitrates.forEach((bitrate, index) => {
            const option = document.createElement('option');
            option.value = index;
            option.text = `${bitrate.height}p (${Math.round(bitrate.bitrate / 1000)}kbps)`;
            select.appendChild(option);
        });
    }
}

Next steps

Support

Need help with your implementation?