Skip to content

GWP Multiplexing Sample

Andre Lafleur edited this page Dec 11, 2025 · 3 revisions

This sample demonstrates how to display multiple cameras simultaneously over a single WebSocket connection using GWP's multiplexing feature. This is ideal for multi-camera grids, video walls, and surveillance dashboards.

What is Multiplexing?

Multiplexing allows multiple GWP players to share a single WebSocket connection to the Media Gateway instead of creating one connection per player. This provides:

  • Reduced Connection Overhead - Only one WebSocket instead of N connections
  • Better Performance - Lower network and server resource usage
  • Scalability - Handle more simultaneous cameras
  • Simplified Management - One shared service manages all streams

Features

This sample application includes:

  • Flexible Grid Layouts - 2x2 (4 cameras), 3x3 (9 cameras), or 4x4 (16 cameras)
  • Shared WebSocket - All cameras use one connection via buildMediaGatewayService()
  • Individual Control - Each player can be controlled independently
  • Dynamic Camera Configuration - Add/remove cameras by GUID
  • Visual Status Indicators - See connection status for each camera
  • Real-time Statistics - Monitor connections, active players, and streaming count
  • Error Handling - Per-camera error detection and display

User Interface

The sample provides:

┌─────────────────────────────────────────┐
│  Connection Settings & Camera GUIDs     │
└─────────────────────────────────────────┘
┌─────────────┬─────────────┬─────────────┐
│  Camera 1   │  Camera 2   │  Camera 3   │
│  [Status]   │  [Status]   │  [Status]   │
├─────────────┼─────────────┼─────────────┤
│  Camera 4   │  Camera 5   │  Camera 6   │
│  [Status]   │  [Status]   │  [Status]   │
└─────────────┴─────────────┴─────────────┘
┌─────────────────────────────────────────┐
│  Statistics & Information               │
└─────────────────────────────────────────┘

Each camera tile shows:

  • Camera number/label
  • Connection status (Waiting, Connecting, Streaming, Error)
  • Color-coded border (orange=connecting, green=streaming, red=error)

Complete Source Code

Save the following as an HTML file (e.g., gwp-multiplexing.html) and open it in a web browser:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>GWP Multiplexing Demo - Multiple Cameras Grid</title>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }

    body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      background-color: #1a1a1a;
      color: #fff;
      padding: 20px;
    }

    .container {
      max-width: 1400px;
      margin: 0 auto;
    }

    header {
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      padding: 20px 30px;
      border-radius: 8px;
      margin-bottom: 20px;
    }

    header h1 {
      font-size: 24px;
      margin-bottom: 5px;
    }

    header p {
      font-size: 14px;
      opacity: 0.9;
    }

    .controls {
      background: #2d2d2d;
      padding: 20px;
      border-radius: 8px;
      margin-bottom: 20px;
    }

    .form-group {
      margin-bottom: 15px;
    }

    label {
      display: block;
      font-size: 13px;
      font-weight: 500;
      margin-bottom: 5px;
      color: #ccc;
    }

    input[type="text"],
    input[type="password"],
    select {
      width: 100%;
      padding: 8px 10px;
      border: 1px solid #444;
      border-radius: 4px;
      font-size: 13px;
      background: #1a1a1a;
      color: #fff;
      font-family: inherit;
    }

    input[type="text"]:focus,
    input[type="password"]:focus,
    select:focus {
      outline: none;
      border-color: #667eea;
      box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
    }

    .camera-inputs {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
      gap: 15px;
      margin: 15px 0;
    }

    button {
      padding: 10px 20px;
      border: none;
      border-radius: 4px;
      font-size: 14px;
      font-weight: 500;
      cursor: pointer;
      transition: all 0.2s;
      margin-right: 10px;
    }

    .btn-primary {
      background-color: #667eea;
      color: white;
    }

    .btn-primary:hover {
      background-color: #5568d3;
    }

    .btn-danger {
      background-color: #f44336;
      color: white;
    }

    .btn-danger:hover {
      background-color: #da190b;
    }

    .btn-secondary {
      background-color: #6c757d;
      color: white;
    }

    .btn-secondary:hover {
      background-color: #5a6268;
    }

    .grid-controls {
      display: flex;
      gap: 10px;
      align-items: center;
      margin-top: 15px;
    }

    .camera-grid {
      display: grid;
      gap: 10px;
      background: #2d2d2d;
      padding: 10px;
      border-radius: 8px;
      margin-bottom: 20px;
    }

    .camera-grid.grid-2x2 {
      grid-template-columns: repeat(2, 1fr);
      grid-template-rows: repeat(2, 300px);
    }

    .camera-grid.grid-3x3 {
      grid-template-columns: repeat(3, 1fr);
      grid-template-rows: repeat(3, 250px);
    }

    .camera-grid.grid-4x4 {
      grid-template-columns: repeat(4, 1fr);
      grid-template-rows: repeat(4, 200px);
    }

    .camera-tile {
      background: #000;
      border: 2px solid #444;
      border-radius: 4px;
      position: relative;
      overflow: hidden;
    }

    .camera-tile:hover {
      border-color: #667eea;
    }

    .camera-tile.error {
      border-color: #f44336;
    }

    .camera-tile.loading {
      border-color: #ff9800;
    }

    .camera-label {
      position: absolute;
      top: 5px;
      left: 5px;
      background: rgba(0, 0, 0, 0.7);
      padding: 3px 8px;
      border-radius: 3px;
      font-size: 11px;
      z-index: 10;
    }

    .camera-status {
      position: absolute;
      bottom: 5px;
      right: 5px;
      background: rgba(0, 0, 0, 0.7);
      padding: 3px 8px;
      border-radius: 3px;
      font-size: 10px;
      z-index: 10;
    }

    .status-connected {
      color: #4CAF50;
    }

    .status-connecting {
      color: #ff9800;
    }

    .status-error {
      color: #f44336;
    }

    .player-container {
      width: 100%;
      height: 100%;
    }

    .info-panel {
      background: #2d2d2d;
      padding: 15px;
      border-radius: 8px;
      font-size: 13px;
    }

    .info-panel h3 {
      margin-bottom: 10px;
      color: #667eea;
    }

    .info-panel ul {
      margin-left: 20px;
    }

    .info-panel li {
      margin-bottom: 5px;
      line-height: 1.6;
    }

    .stats {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
      gap: 15px;
      margin-top: 15px;
    }

    .stat-card {
      background: #1a1a1a;
      padding: 15px;
      border-radius: 6px;
      border-left: 3px solid #667eea;
    }

    .stat-label {
      font-size: 11px;
      color: #999;
      text-transform: uppercase;
      margin-bottom: 5px;
    }

    .stat-value {
      font-size: 20px;
      font-weight: 600;
    }

    .note {
      background: #3d3d00;
      border-left: 3px solid #ffeb3b;
      padding: 10px 15px;
      margin-top: 15px;
      border-radius: 4px;
      font-size: 12px;
    }
  </style>
</head>
<body>

  <div class="container">
    <header>
      <h1>🎥 GWP Multiplexing Demo</h1>
      <p>Demonstrate multiple cameras over a single WebSocket connection</p>
    </header>

    <div class="controls">
      <h3 style="margin-bottom: 15px;">Connection Settings</h3>

      <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
        <div class="form-group">
          <label for="mediaGateway">Media Gateway Address</label>
          <input type="text" id="mediaGateway" placeholder="localhost or IP" value="127.0.0.1">
        </div>

        <div class="form-group">
          <label for="username">Username</label>
          <input type="text" id="username" value="admin">
        </div>

        <div class="form-group">
          <label for="appId">SDK Certificate</label>
          <input type="text" id="appId" value="KxsD11z743Hf5Gq9mv3+5ekxzemlCiUXkTFY5ba1NOGcLCmGstt2n0zYE9NsNimv">
        </div>

        <div class="form-group">
          <label for="password">Password</label>
          <input type="password" id="password">
        </div>
      </div>

      <h3 style="margin: 20px 0 15px 0;">Camera GUIDs</h3>

      <div class="camera-inputs" id="cameraInputs">
        <div class="form-group">
          <label for="camera1">Camera 1 GUID</label>
          <input type="text" id="camera1" placeholder="00000000-0000-0000-0000-000000000000">
        </div>
        <div class="form-group">
          <label for="camera2">Camera 2 GUID</label>
          <input type="text" id="camera2" placeholder="00000000-0000-0000-0000-000000000000">
        </div>
        <div class="form-group">
          <label for="camera3">Camera 3 GUID</label>
          <input type="text" id="camera3" placeholder="00000000-0000-0000-0000-000000000000">
        </div>
        <div class="form-group">
          <label for="camera4">Camera 4 GUID</label>
          <input type="text" id="camera4" placeholder="00000000-0000-0000-0000-000000000000">
        </div>
      </div>

      <div class="grid-controls">
        <label for="gridSize">Grid Size:</label>
        <select id="gridSize">
          <option value="2x2" selected>2x2 (4 cameras)</option>
          <option value="3x3">3x3 (9 cameras)</option>
          <option value="4x4">4x4 (16 cameras)</option>
        </select>

        <button id="startBtn" class="btn-primary">Start All Cameras</button>
        <button id="stopBtn" class="btn-danger" disabled>Stop All Cameras</button>
      </div>

      <div class="note">
        <strong>Note:</strong> Leave camera GUIDs empty to skip those tiles. The demo will only start players for cameras with valid GUIDs.
      </div>
    </div>

    <div class="camera-grid grid-2x2" id="cameraGrid">
      <!-- Camera tiles will be generated dynamically -->
    </div>

    <div class="info-panel">
      <h3>📊 Statistics</h3>
      <div class="stats">
        <div class="stat-card">
          <div class="stat-label">WebSocket Connections</div>
          <div class="stat-value" id="statConnections">0</div>
        </div>
        <div class="stat-card">
          <div class="stat-label">Active Players</div>
          <div class="stat-value" id="statPlayers">0</div>
        </div>
        <div class="stat-card">
          <div class="stat-label">Streaming Cameras</div>
          <div class="stat-value" id="statStreaming">0</div>
        </div>
        <div class="stat-card">
          <div class="stat-label">GWP Version</div>
          <div class="stat-value" id="statVersion">-</div>
        </div>
      </div>

      <h3 style="margin-top: 20px;">ℹ️ How Multiplexing Works</h3>
      <ul>
        <li><strong>Single WebSocket:</strong> All players share one WebSocket connection to the Media Gateway</li>
        <li><strong>Shared Service:</strong> Created using <code>gwp.buildMediaGatewayService()</code></li>
        <li><strong>Individual Control:</strong> Each player can be controlled independently (play, pause, seek)</li>
        <li><strong>Resource Efficiency:</strong> Reduces network overhead and connection count</li>
        <li><strong>Scaling:</strong> Better performance when displaying many cameras simultaneously</li>
        <li><strong>Shared Fate:</strong> If the WebSocket fails, all players are affected</li>
      </ul>

      <h3 style="margin-top: 20px;">🔧 Code Pattern</h3>
      <pre style="background: #1a1a1a; padding: 15px; border-radius: 4px; overflow-x: auto; font-size: 12px; margin-top: 10px;">
// 1. Create shared Media Gateway service
const service = await gwp.buildMediaGatewayService(
  mediaGatewayUrl,
  getTokenFunction
);

// 2. Build individual players
const player1 = gwp.buildPlayer(container1);
const player2 = gwp.buildPlayer(container2);

// 3. Start players with the shared service
await player1.startWithService(cameraGuid1, service);
await player2.startWithService(cameraGuid2, service);

// 4. Control each player independently
player1.playLive();
player2.playLive();
      </pre>
    </div>
  </div>

  <script>
    // ============================================================================
    // GLOBAL STATE
    // ============================================================================
    let mediaGatewayService = null;
    let players = [];
    let gwpScript = null;
    let gridSize = '2x2';

    // ============================================================================
    // UTILITY FUNCTIONS
    // ============================================================================

    function updateStats() {
      const activeCount = players.filter(p => p.player?.isStarted).length;
      const streamingCount = players.filter(p => p.player?.isStarted && !p.player?.isPaused).length;

      document.getElementById('statConnections').textContent = mediaGatewayService ? '1' : '0';
      document.getElementById('statPlayers').textContent = activeCount;
      document.getElementById('statStreaming').textContent = streamingCount;
    }

    function updateTileStatus(index, status, message) {
      const tile = document.getElementById(`tile-${index}`);
      if (!tile) return;

      const statusEl = tile.querySelector('.camera-status');
      if (statusEl) {
        statusEl.textContent = message;
        statusEl.className = 'camera-status status-' + status;
      }

      tile.className = 'camera-tile ' + status;
    }

    async function getToken(cameraId) {
      const username = document.getElementById('username').value.trim();
      const appId = document.getElementById('appId').value.trim();
      const password = document.getElementById('password').value.trim();
      const mediaGateway = document.getElementById('mediaGateway').value.trim();
      const credentials = `${username};${appId}:${password}`;

      try {
        const response = await fetch(`https://${mediaGateway}/media/v2/token/${cameraId}`, {
          credentials: 'include',
          headers: { 'Authorization': `Basic ${btoa(credentials)}` }
        });

        if (!response.ok) {
          throw new Error(`${response.status} ${response.statusText}`);
        }

        return await response.text();
      } catch (error) {
        console.error(`Token retrieval failed for ${cameraId}:`, error.message);
        throw error;
      }
    }

    // ============================================================================
    // GRID MANAGEMENT
    // ============================================================================

    function createGrid(size) {
      gridSize = size;
      const grid = document.getElementById('cameraGrid');
      grid.className = 'camera-grid grid-' + size;
      grid.innerHTML = '';

      const counts = {
        '2x2': 4,
        '3x3': 9,
        '4x4': 16
      };

      const tileCount = counts[size];

      // Generate camera input fields if needed
      const inputsContainer = document.getElementById('cameraInputs');
      inputsContainer.innerHTML = '';

      for (let i = 1; i <= tileCount; i++) {
        // Create input field
        const formGroup = document.createElement('div');
        formGroup.className = 'form-group';
        formGroup.innerHTML = `
          <label for="camera${i}">Camera ${i} GUID</label>
          <input type="text" id="camera${i}" placeholder="00000000-0000-0000-0000-000000000000">
        `;
        inputsContainer.appendChild(formGroup);

        // Create grid tile
        const tile = document.createElement('div');
        tile.className = 'camera-tile';
        tile.id = `tile-${i}`;
        tile.innerHTML = `
          <div class="camera-label">Camera ${i}</div>
          <div class="camera-status status-connecting">Waiting...</div>
          <div class="player-container" id="player-${i}"></div>
        `;
        grid.appendChild(tile);
      }
    }

    // ============================================================================
    // PLAYER MANAGEMENT
    // ============================================================================

    async function startAllCameras() {
      const mediaGateway = document.getElementById('mediaGateway').value.trim();

      if (!mediaGateway) {
        alert('Please enter Media Gateway address');
        return;
      }

      try {
        document.getElementById('startBtn').disabled = true;
        updateStats();

        // Load gwp.js
        console.log('Loading GWP library...');
        gwpScript = document.createElement('script');
        gwpScript.src = `https://${mediaGateway}/media/v2/files/gwp.js`;

        await new Promise((resolve, reject) => {
          gwpScript.onload = resolve;
          gwpScript.onerror = () => reject(new Error('Failed to load gwp.js'));
          document.body.appendChild(gwpScript);
        });

        console.log('GWP library loaded');

        // Get GWP version
        if (typeof gwp !== 'undefined') {
          const version = gwp.version();
          document.getElementById('statVersion').textContent = version;
          console.log('GWP Version:', version);
        }

        // Create shared Media Gateway service
        console.log('Creating Media Gateway service...');
        mediaGatewayService = await gwp.buildMediaGatewayService(
          `https://${mediaGateway}/media`,
          getToken
        );
        console.log('Media Gateway service created (1 WebSocket connection)');
        updateStats();

        // Get camera GUIDs
        const counts = { '2x2': 4, '3x3': 9, '4x4': 16 };
        const tileCount = counts[gridSize];
        const cameraGuids = [];

        for (let i = 1; i <= tileCount; i++) {
          const guid = document.getElementById(`camera${i}`).value.trim();
          cameraGuids.push(guid || null);
        }

        // Start players for each camera
        for (let i = 0; i < tileCount; i++) {
          const cameraGuid = cameraGuids[i];
          const playerIndex = i + 1;

          if (!cameraGuid) {
            updateTileStatus(playerIndex, 'error', 'No GUID');
            players.push({ player: null, cameraGuid: null });
            continue;
          }

          try {
            updateTileStatus(playerIndex, 'connecting', 'Connecting...');

            const container = document.getElementById(`player-${playerIndex}`);
            const player = gwp.buildPlayer(container);

            // Register error handler
            player.onErrorStateRaised.register((error) => {
              console.error(`Camera ${playerIndex} error:`, error.value);
              updateTileStatus(playerIndex, 'error', `Error: ${error.errorCode}`);
            });

            // Register stream status handler
            player.onStreamStatusChanged.register((event) => {
              if (event.state === 5) { // Streaming
                updateTileStatus(playerIndex, 'connected', 'Streaming');
              }
            });

            // Start player with shared service
            await player.startWithService(cameraGuid, mediaGatewayService);
            player.playLive();

            players.push({ player, cameraGuid });
            updateTileStatus(playerIndex, 'connected', 'Connected');
            console.log(`Camera ${playerIndex} started successfully`);

          } catch (error) {
            console.error(`Failed to start camera ${playerIndex}:`, error.message);
            updateTileStatus(playerIndex, 'error', error.message);
            players.push({ player: null, cameraGuid });
          }
        }

        updateStats();
        document.getElementById('stopBtn').disabled = false;
        console.log(`Started ${players.filter(p => p.player).length} players over 1 WebSocket connection`);

      } catch (error) {
        console.error('Failed to start cameras:', error);
        alert(`Error: ${error.message}`);
        document.getElementById('startBtn').disabled = false;
      }
    }

    function stopAllCameras() {
      console.log('Stopping all cameras...');

      // Stop and dispose all players
      players.forEach((p, index) => {
        if (p.player) {
          try {
            p.player.stop();
            p.player.dispose();
            updateTileStatus(index + 1, 'error', 'Stopped');
          } catch (error) {
            console.error(`Error stopping camera ${index + 1}:`, error);
          }
        }
      });

      players = [];
      mediaGatewayService = null;

      updateStats();
      document.getElementById('startBtn').disabled = false;
      document.getElementById('stopBtn').disabled = true;

      console.log('All cameras stopped');
    }

    // ============================================================================
    // EVENT LISTENERS
    // ============================================================================

    document.addEventListener('DOMContentLoaded', () => {
      // Initialize grid
      createGrid('2x2');

      // Grid size change
      document.getElementById('gridSize').addEventListener('change', (e) => {
        if (players.length > 0) {
          if (!confirm('Changing grid size will stop all cameras. Continue?')) {
            e.target.value = gridSize;
            return;
          }
          stopAllCameras();
        }
        createGrid(e.target.value);
      });

      // Start/Stop buttons
      document.getElementById('startBtn').addEventListener('click', startAllCameras);
      document.getElementById('stopBtn').addEventListener('click', stopAllCameras);

      console.log('Multiplexing demo initialized');
    });
  </script>
</body>
</html>

Usage Instructions

Getting Started

  1. Enter Connection Details

    • Media Gateway Address - Your Media Gateway hostname or IP
    • Username - Security Center username
    • SDK Certificate - Your application's SDK certificate
    • Password - Security Center password
  2. Configure Cameras

    • Enter camera GUIDs for the cameras you want to display
    • Leave fields empty to skip those positions
    • You can enter fewer cameras than the grid size
  3. Select Grid Size

    • Choose 2x2 (4 cameras), 3x3 (9 cameras), or 4x4 (16 cameras)
    • Changing grid size while running will stop all cameras
  4. Start Cameras

    • Click "Start All Cameras"
    • Watch the status indicators for each camera
    • Monitor the statistics panel to see connection count (should be 1)

Monitoring

Camera Status Indicators:

  • Waiting (Gray) - No GUID provided or waiting to start
  • Connecting (Orange border) - Attempting to connect
  • Streaming (Green border) - Successfully streaming video
  • Error (Red border) - Connection or stream error

Statistics Display:

  • WebSocket Connections - Should always show "1" when running
  • Active Players - Number of successfully started players
  • Streaming Cameras - Number of cameras actively streaming
  • GWP Version - Version of the loaded GWP library

Key Implementation Details

Creating the Shared Service

// Create one shared service for all players
const mediaGatewayService = await gwp.buildMediaGatewayService(
  `https://${mediaGateway}/media`,
  getToken
);

Starting Players with the Service

// Build individual players
const player = gwp.buildPlayer(containerElement);

// Start with the shared service (NOT player.start())
await player.startWithService(cameraGuid, mediaGatewayService);

// Play live
player.playLive();

Error Handling Per Camera

// Each player has independent error handling
player.onErrorStateRaised.register((error) => {
  console.error(`Camera ${index} error:`, error.value);
  updateTileStatus(index, 'error', `Error: ${error.errorCode}`);
});

Benefits vs. Individual Connections

Feature Multiplexing Individual Connections
WebSocket Connections 1 (shared) N (one per player)
Network Overhead Low High (N connections)
Media Gateway Load Lower Higher
Scalability Better (more cameras) Limited by connection count
Failure Impact All players affected Only failed player affected
Setup Complexity Slightly more complex Simpler

When to Use Multiplexing

Use multiplexing when:

  • Displaying 4+ cameras simultaneously
  • Building video walls or dashboards
  • Connection count is limited
  • Network efficiency is important
  • All cameras connect to the same Media Gateway

Use individual connections when:

  • Displaying 1-3 cameras
  • Cameras connect to different Media Gateways
  • Isolation is critical (one failure shouldn't affect others)
  • Simplicity is preferred over efficiency

Limitations

  • All players must connect to the same Media Gateway
  • If the shared WebSocket fails, all players are affected
  • Cannot mix start() and startWithService() on the same service
  • Slightly more complex error recovery

See Also

Security Center SDK


Macro SDK Developer Guide


Web SDK Developer Guide

  • Getting Started Setup, authentication, and basic configuration for the Web SDK.
  • Referencing Entities Entity discovery, search capabilities, and parameter formats.
  • Entity Operations CRUD operations, multi-value fields, and method execution.
  • Partitions Managing partitions, entity membership, and user access control.
  • Custom Fields Creating, reading, writing, and filtering custom entity fields.
  • Custom Card Formats Managing custom credential card format definitions.
  • Actions Control operations for doors, cameras, macros, and notifications.
  • Events and Alarms Real-time event monitoring, alarm monitoring, and custom events.
  • Incidents Incident management, creation, and attachment handling.
  • Reports Activity reports, entity queries, and historical data retrieval.
  • Performance Guide Optimization tips and best practices for efficient API usage.
  • Reference Entity GUIDs, EntityType enumeration, and EventType enumeration.
  • Under the Hood Technical architecture, query reflection, and SDK internals.
  • Troubleshooting Common error resolution and debugging techniques.

Media Gateway Developer Guide


Web Player Developer Guide

Clone this wiki locally