The Nx Witness API allows third-party web browsers to send audio to a camera and receive audio from a camera, enabling 2-way audio communication through security cameras.
Requirements
- The camera must support two-way audio and be supported by Network Optix. Check the IPVD list with the 2-way audio tag enabled.
- The requesting user needs the access right that covers 2-way audio on the device.
How does it work?
Basic format for a request from browser to camera
To send audio to a camera, establish a WebSocket connection to the audio backchannel endpoint:
ws://<server_ip>:<server_port>/api/http_audio?camera_id=<id>&format=<audio_sample_format>&sample_rate=<audio_sample_rate>&channels=<audio_channels_count>
Replace the <server_ip>, <server_port>, and <id> with the relevant information. <id> should reflect the Camera ID of the camera you wish to use.
When using a raw audio format (PCM), include the following parameters:
-
<audio_sample_format>— use one of the following formats: u8, s16be, s16le, s32be, s32le, f32be, f32le, f64be, and f64le. -
<audio_sample_rate>— input the desired audio sample rate, which is an integer value. -
<audio_channels_count>— use 1 for mono and 2 for stereo.
Note: If you are sending audio wrapped in a media container (such as MP4, WAV, WebM, or AAC), omit these parameters. The server detects container formats automatically.
Example of a browser request:
ws://localhost:7001/api/http_audio?camera_id=2a4717bb-1d3e-4878-a28b-af4eaedbfb89&format=f32le&sample_rate=44100&channels=1
Basic format for a request from camera to browser
To stream audio from the camera, request the WebM media endpoint:
http://<server_ip>:<server_port>/rest/v4/devices/<camera_id>/media.webm?audioOnly=true
Example
The code below is a sample HTML page that demonstrates the functionality of using 2-way audio in the browser through the API.
To test the sample below, fill in your server and port information, credentials, and camera ID, then use the buttons. Allow microphone access when prompted.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Nx two-way audio demo</title>
</head>
<body>
Server (host:port): <input type="text" id="server" size="40" value="localhost:7001"><br>
User: <input type="text" id="user" size="40" value="admin"><br>
Password: <input type="text" id="pass" size="40"><br>
Camera ID: <input type="text" id="camera_id" size="40"><br>
<h2>Microphone</h2>
<button onclick="startMic()">Start mic</button>
<button onclick="stopMic()">Stop</button>
<h2>Send an audio file</h2>
<input type="file" id="filename">
<button onclick="sendFile()">Connect & upload</button>
<h2>Listen to camera</h2>
<button onclick="listen()">Load camera audio</button><br>
<audio id="cameraAudio" controls></audio>
<script>
let ws = null, audioContext = null, micStream = null, workletNode = null;
let sessionToken = null;
const WORKLET_CODE = `
class MicSender extends AudioWorkletProcessor {
constructor() { super(); this._buf = new Float32Array(4096); this._n = 0; }
process(inputs) {
const ch = inputs[0] && inputs[0][0];
if (ch) {
for (let i = 0; i < ch.length; i++) {
this._buf[this._n++] = ch[i];
if (this._n === this._buf.length) {
this.port.postMessage(this._buf.slice(0));
this._n = 0;
}
}
}
return true;
}
}
registerProcessor('mic-sender', MicSender);
`;
function base(scheme) {
// Token/bearer auth (login + _sessionToken) is only accepted over TLS,
// so this always uses wss/https — there is no plain-http mode.
const host = document.getElementById('server').value;
return scheme + "s://" + host;
}
function camId() { return encodeURIComponent(document.getElementById('camera_id').value); }
async function getToken() {
if (sessionToken) return sessionToken;
const resp = await fetch(base("http") + "/rest/v4/login/sessions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: document.getElementById('user').value,
password: document.getElementById('pass').value,
durationS: 3600
})
});
if (!resp.ok) throw new Error("login failed: HTTP " + resp.status);
const data = await resp.json();
sessionToken = data.token;
console.log("logged in; token expires in", data.expiresInS, "s");
return sessionToken;
}
function openSocket(url, onopen) {
console.log("connecting to", url.replace(/_sessionToken=[^&]+/, "_sessionToken=***"));
ws = new WebSocket(url);
ws.binaryType = "arraybuffer";
ws.onopen = () => { console.log("connected"); if (onopen) onopen(); };
ws.onclose = (e) => console.log("closed", e.code, e.reason);
ws.onerror = () => console.log("WebSocket error");
}
async function startMic() {
let tok;
try { tok = await getToken(); }
catch (e) { console.log("auth failed:", e); return; }
let stream;
try { stream = await navigator.mediaDevices.getUserMedia({ audio: true }); }
catch (e) { console.log("microphone error:", e); return; }
micStream = stream;
audioContext = new AudioContext();
await audioContext.resume();
const rate = audioContext.sampleRate;
const moduleUrl = URL.createObjectURL(new Blob([WORKLET_CODE], { type: "application/javascript" }));
await audioContext.audioWorklet.addModule(moduleUrl);
URL.revokeObjectURL(moduleUrl);
const url = base("ws") + "/api/http_audio?camera_id=" + camId()
+ "&_sessionToken=" + encodeURIComponent(tok)
+ "&format=f32le&sample_rate=" + rate + "&channels=1";
openSocket(url, () => {
const source = audioContext.createMediaStreamSource(stream);
workletNode = new AudioWorkletNode(audioContext, "mic-sender");
workletNode.port.onmessage = (e) => {
if (ws && ws.readyState === WebSocket.OPEN) ws.send(e.data);
};
source.connect(workletNode);
const mute = audioContext.createGain();
mute.gain.value = 0;
workletNode.connect(mute).connect(audioContext.destination);
});
}
function stopMic() {
if (workletNode) { workletNode.disconnect(); workletNode = null; }
if (micStream) { micStream.getTracks().forEach(t => t.stop()); micStream = null; }
if (audioContext) { audioContext.close(); audioContext = null; }
if (ws) { ws.close(); ws = null; }
}
async function sendFile() {
const file = document.getElementById('filename').files[0];
if (!file) { console.log("choose a file first"); return; }
let tok;
try { tok = await getToken(); }
catch (e) { console.log("auth failed:", e); return; }
const url = base("ws") + "/api/http_audio?camera_id=" + camId()
+ "&_sessionToken=" + encodeURIComponent(tok);
openSocket(url, () => {
const reader = new FileReader();
reader.onload = (e) => {
ws.send(e.target.result);
// "Upload complete" = the browser has flushed every byte to the network
// (bufferedAmount drains to 0). Only then close, which is the end-of-file
// signal that makes the server play the file.
const timer = setInterval(() => {
if (ws.readyState !== WebSocket.OPEN) { clearInterval(timer); return; } // errored/closed early
if (ws.bufferedAmount === 0) {
clearInterval(timer);
console.log("upload complete:", e.target.result.byteLength, "bytes");
ws.close();
}
}, 50);
};
reader.readAsArrayBuffer(file);
});
}
async function listen() {
let tok;
try { tok = await getToken(); }
catch (e) { console.log("auth failed:", e); return; }
const url = base("http") + "/rest/v4/devices/" + camId()
+ "/media.webm?audioOnly=true&_sessionToken=" + encodeURIComponent(tok);
const audio = document.getElementById('cameraAudio');
audio.src = url;
audio.play().catch(e => console.log("play() was blocked:", e));
}
</script>
</body>
</html>
Comments
0 comments
Article is closed for comments.