This tutorial builds on the Getting started with the JavaScript API article. You will expand your single-page integration into a functional panel that displays a real-time camera list and lets you add cameras directly to your current layout.
Overview
In the initial sample, you verified that a web page can communicate with the Nx Witness Desktop Client. In this tutorial, you will extend that connection to:
Fetch and display a real-time list of available cameras.
Automatically update the list when cameras are added or removed.
Collapse lists longer than 5 cameras behind a toggle button.
Click any camera to add it to your active layout.
Architectural changes
The previous tutorial kept all code inside a single index.html file. This tutorial splits the project into modular files using TypeScript and Parcel:
src/index.html: Defines the page layout and UI elements.src/index.ts: Handles the main entry point and API initialization callback.src/initHandlers.ts: Manages resource fetching and event listeners.src/helpers.ts: Handles DOM updates, camera filtering, and click actions.src/typesFromDesktopClient.d.ts: Provides type definitions forwindow.vms.
Prerequisites
Before starting, ensure you have the following:
Complete the Getting started with the JavaScript API sample.
Install Node.js (v18 or higher).
Access to the Nx Witness Desktop Client with API access enabled.
Sample for download
Please refer to the Nx Open Github Repo - js_api_examples
Step 1: Set up the project directory
Set up your project workspace manually or clone the js_api_examples repository from GitHub.
-
Create a project directory named
first_integrationwith the following structure:
first_integration/ ├── package.json └── src/ ├── index.html ├── index.ts ├── initHandlers.ts ├── helpers.ts ├── typesFromDesktopClient.d.ts ├── css/ │ └── styles.css └── img/ └── integration_inclient_js_api.png -
Create a
package.jsonfile in the root directory:
{ "name": "nx-witness-jsapi-first-integration", "version": "1.0.0", "description": "First integration for the Nx Witness In-Client JavaScript API.", "type": "module", "source": "src/index.html", "scripts": { "start": "parcel", "build": "rm -rf dist && parcel build", "serve": "serve dist" }, "author": "Network Optix Inc.", "license": "MPL-2.0", "dependencies": {}, "devDependencies": { "parcel": "^2.0.1", "serve": "^14.2.1" } } -
Install the dependencies:
npm install
Step 2: Create the HTML structure
Create src/index.html. This file updates the initial banner and includes container elements for the dynamic camera list.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>In-Client JavaScript API — First Integration</title>
<link rel="stylesheet" href="./css/style.css">
<script type="module" src="index.ts"></script>
</head>
<body>
<div id="banner" class="viewfinder">
<div class="status" id="statusPillBanner"><span class="dot"></span>JS API Object Initialization Required</div>
<h1>Add this page as an Integration.</h1>
<ol class="steps">
<li>Open Main Menu → Add</li>
<li>Choose Integrations…</li>
</ol>
<img class="frame-img" src="./img/integration_inclient_js_api.png" alt="Steps to add this page as an integration in Desktop Client">
</div>
<div id="app" class="viewfinder" style="display:none">
<div class="status is-live" id="statusPillApp"><span class="dot"></span>JS API Object Initialized completely!</div>
<h1>Congratulations!</h1>
<p id="hello"></p>
<h3 class="section-title">Cameras <span class="count" id="cameraCount">(0)</span></h3>
<p class="hint">Click a camera to add it to this layout.</p>
<ul id="cameraList"></ul>
<button type="button" id="showAllToggle">Show all cameras ↓</button>
</div>
</body>
</html>
Step 3: Define TypeScript interfaces
Create src/typesFromDesktopClient.d.ts to define the shape of window.vms. This provides autocompletion and type checking across your project.
export {};
declare global {
interface Resource {
id: string;
type: string;
name: string;
}
interface Signal<Callback> {
connect(callback: Callback): void;
}
interface VmsResourcesApi {
resources(): Promise<Resource[]>;
added: Signal<(resource: Resource) => void>;
removed: Signal<(resourceId: string) => void>;
hasMediaStream(resourceId: string): Promise<boolean>;
}
interface VmsError {
code: number;
description?: string;
}
interface AddItemResult {
error: VmsError;
item?: { id: string };
}
interface VmsTab {
name: string;
addItem(resourceId: string, params: Record<string, unknown>): Promise<AddItemResult>;
}
interface VmsLogApi {
info(message: string): void;
}
interface VmsApi {
resources: VmsResourcesApi;
tab: VmsTab;
log: VmsLogApi;
ErrorCode: { success: number; [key: string]: number };
}
interface Window {
isVmsApiEnabled?: boolean;
vmsApiInit?: () => void | Promise<void>;
vms: VmsApi;
}
}Step 4: Implement helper functions
Create src/helpers.ts. This file contains utility functions to render rows, update visibility limits, and handle layout insertion when a user clicks a camera.
const VISIBLE_LIMIT = 5;
const findRow = (list: HTMLElement, resourceId: string): HTMLLIElement | undefined =>
Array.from(list.children).find(
(child) => (child as HTMLElement).dataset.id === resourceId
) as HTMLLIElement | undefined;
export const addResourceToLayout = async (resourceId: string) => {
const result = await window.vms.tab.addItem(resourceId, {});
if (result.error.code === window.vms.ErrorCode.success) {
window.vms.log.info(`Added item ${result.item?.id} for resource ${resourceId} to the layout.`);
} else {
window.vms.log.info(`Could not add resource ${resourceId} to the layout. Error code: ${result.error.code}`);
}
};
export const refreshVisibility = (list: HTMLElement, toggle: HTMLElement) => {
const rows = Array.from(list.children) as HTMLElement[];
rows.forEach((row, index) => row.classList.toggle("extra", index >= VISIBLE_LIMIT));
const countEl = document.getElementById("cameraCount");
if (countEl) countEl.textContent = `(${rows.length})`;
if (rows.length > VISIBLE_LIMIT) {
toggle.style.display = "inline-flex";
} else {
toggle.style.display = "none";
list.classList.remove("expanded");
}
};
export const addOrUpdateResourceRow = (list: HTMLElement, toggle: HTMLElement, resource: Resource) => {
let row = findRow(list, resource.id);
if (!row) {
row = document.createElement("li");
row.dataset.id = resource.id;
row.tabIndex = 0;
const tag = document.createElement("span");
tag.className = "tag";
row.appendChild(tag);
row.appendChild(document.createTextNode(""));
row.addEventListener("click", () => addResourceToLayout(resource.id));
list.appendChild(row);
}
(row.querySelector(".tag") as HTMLElement).textContent = resource.type;
row.lastChild!.textContent = resource.name;
refreshVisibility(list, toggle);
};
export const removeResourceRow = (list: HTMLElement, toggle: HTMLElement, resourceId: string) => {
findRow(list, resourceId)?.remove();
refreshVisibility(list, toggle);
};Step 5: Fetch and listen for resources
Create src/initHandlers.ts. This module fetches the initial camera list and subscribes to API signals (added and removed) to handle real-time system changes.
import { addOrUpdateResourceRow, removeResourceRow } from "./helpers";
export const initResourcesUI = async (list: HTMLElement, toggle: HTMLElement) => {
const addIfCamera = async (resource: Resource) => {
const hasMedia = await window.vms.resources.hasMediaStream(resource.id);
if (hasMedia) addOrUpdateResourceRow(list, toggle, resource);
};
// Connect event listeners before fetching the initial list to avoid race conditions.
window.vms.resources.added.connect(addIfCamera);
window.vms.resources.removed.connect((resourceId) => removeResourceRow(list, toggle, resourceId));
const resources = await window.vms.resources.resources();
await Promise.all(resources.map(addIfCamera));
};Step 6: Initialize the integration
Create src/index.ts. This file manages view toggles, binds event listeners to UI elements, and invokes initResourcesUI when the Desktop Client API is ready.
import { initResourcesUI } from "./initHandlers";
function showApp(isConnected: boolean) {
document.getElementById("banner")!.style.display = isConnected ? "none" : "block";
document.getElementById("app")!.style.display = isConnected ? "block" : "none";
}
if (!window.isVmsApiEnabled) {
window.onload = () => showApp(false);
}
const cameraList = document.getElementById("cameraList") as HTMLUListElement;
const showAllToggle = document.getElementById("showAllToggle") as HTMLButtonElement;
showAllToggle.addEventListener("click", (event) => {
const expanded = cameraList.classList.toggle("expanded");
(event.currentTarget as HTMLButtonElement).textContent = expanded
? "Show fewer cameras ↑"
: "Show all cameras ↓";
});
window.vmsApiInit = async () => {
showApp(true);
const helloEl = document.getElementById("hello")!;
helloEl.innerHTML = `This Integration is now running inside tab <span>${window.vms.tab.name}</span>.`;
await initResourcesUI(cameraList, showAllToggle);
};Step 7: Test your integration
-
Start the local development server:
npm startParcel serves the application at
http://localhost:1234. Open the Nx Witness Desktop Client.
Select Main Menu → Add → Integrations….
Add
http://localhost:1234as a new integration.Open the integration tab and drag it into the viewing layout.
The camera list renders inside your tab. Select any item in the list to place that camera stream onto your active layout.
Deploying to production
To prepare your integration for production deployment:
-
Build the production bundle:
npm run build -
Serve the static contents of the
distdirectory using your web server or test locally:npm run serve Update the Integration URL in the Nx Witness Desktop Client to point to your hosted endpoint (for example,
http://localhost:3000).
Next steps
Now that you can list resources and place items on layouts, explore these additional API namespaces:
window.vms.auth: Acquire session and cloud tokens for external API calls.window.vms.self.setMinimalInterfaceMode(true): Hide standard web view UI elements to streamline integration layout space.
Comments
0 comments
Article is closed for comments.