Intercom / SIP API for Web Developers
Engineered primarily for AVS-10 and AVS-15 Touch Panels equipped with onboard dual microphones, stereo speakers, and a 5 MP front camera. Also supported on standard Android tablets and phones running App Container (not currently available on iOS).
App Container features a built-in SIP / intercom engine supporting both peer-to-peer (P2P) direct calling between touch panels across the local network and SIP PBX registration (connecting with IP PBX servers, DoorBird/2N door stations, or office extensions).
Web developers can control the intercom engine directly from hosted HTML5 projects via window.appcontainer.sip — placing and answering calls (audio and video), muting the local microphone, adjusting call volume, requesting native video window overlays over DOM elements, triggering broadcast paging ("Page All Panels"), and reacting to real-time telephony events without leaving the web project's UI.
🏗️ Architecture & Trust Model
- Native Audio & Video Rendering: Audio always renders natively, never inside the web page. Video can too, but web applications can also request native video windows positioned over placeholder DOM elements (
<div>withdivIdor fractional screenrect), identical to the Native Player (streamBridge) interaction model. - Control & Telemetry Plane: The JavaScript API is specifically for control, dialing, video window positioning, and status telemetry (the same trust level as
window.appcontainer.gpioBridge). - Always Available: The bridge is accessible immediately when your project loads. It does not require the technician-only Intercom Setup screen to be open.
- Promise & Event Pattern: All dialing and control methods return a
Promiseresolving to the latest status snapshot. Telephony failures (busy, no answer, network dropped) are delivered asynchronously via events (callFailed,callEnded) rather than rejected Promises.
🚀 Placing and Controlling Calls
All control methods return a Promise that resolves to a status snapshot.
// Peer-to-peer: Call another panel directly by local IP address
await window.appcontainer.sip.placeCallToIp('192.168.1.50');
// Via PBX: Call a registered extension (requires SIP Server mode configured in Intercom Setup)
await window.appcontainer.sip.placeCallToExtension('101');
// Answer incoming ringing call
await window.appcontainer.sip.answer();
// Reject incoming call (immediately sends SIP 486 "Busy Here" to caller)
await window.appcontainer.sip.reject();
// Silently dismiss ring locally:
// No response is sent to caller, who keeps ringing until their ~20s timeout.
// No callEnded event fires for this action.
await window.appcontainer.sip.ignore();
// Hang up / end current active call
await window.appcontainer.sip.hangup();
// Broadcast paging: call every panel currently discovered on the local network (P2P mode only)
await window.appcontainer.sip.pageAll();
await window.appcontainer.sip.hangupPageAll();
// Toggle automatic answer for incoming calls (answers immediately without ringing)
await window.appcontainer.sip.setAutoAnswer(true);
// Mute / unmute local microphone during an active call — works for both
// WebRTC calls (peer-to-peer or PBX) and legacy (2N / plain PBX extension)
// calls. Current state is reflected in getStatus().micMuted.
await window.appcontainer.sip.setMicMuted(true);
await window.appcontainer.sip.setMicMuted(false);
// Adjust call audio volume (device-wide, applies equally to WebRTC and
// legacy calls). Range is 0 to getStatus().callMaxVolume.
await window.appcontainer.sip.setCallVolume(5);
📊 Status & Panel Discovery
Getting Intercom Status
Call getStatus() at any time to obtain a comprehensive snapshot of the SIP engine:
const status = await window.appcontainer.sip.getStatus();
console.log('Intercom status:', status);
Status Snapshot Structure
{
"callStatus": "connected (incoming)",
"calleeBusy": false,
"callHasVideo": true,
"activeCallId": "39279a15-2889-1240-a124-001248ac9120",
"incomingCallerName": "Front Door Station",
"incomingIsBroadcast": false,
"registered": true,
"pagingActiveCount": 0,
"deviceName": "Panel-575afc",
"micMuted": false,
"callVolume": 9,
"callMaxVolume": 15
}
Fields Description
| Field | Type | Description |
|---|---|---|
callStatus | string | Current call state (see possible values below). |
calleeBusy | boolean | true if the dialed remote target responded busy. |
callHasVideo | boolean | true if the call includes an active video stream. |
activeCallId | string | null | Unique UUID for the current active/ringing call session. |
incomingCallerName | string | null | Name or caller ID of the incoming caller. |
incomingIsBroadcast | boolean | true if the incoming call is a broadcast page from another panel. |
registered | boolean | PBX registration status (true when successfully registered with SIP server). |
pagingActiveCount | number | Number of currently connected panels during a pageAll() broadcast. |
deviceName | string | Local device network identifier (e.g., Panel-575afc). |
micMuted | boolean | Current local microphone mute status (true when muted). Applies to both WebRTC and legacy calls. |
callVolume | number | Current device call audio volume level (0 to callMaxVolume). |
callMaxVolume | number | Maximum device call audio volume level (device-dependent). |
Possible callStatus Values
idle— No active or pending callsringing— Incoming call is ringing locallycalling...— Dialing outgoing callincoming...— Remote incoming call session initializingconnected (incoming)— Call answered (incoming direction)connected (outgoing)— Call answered (outgoing P2P direction)connected (outgoing via PBX)— Call answered (outgoing PBX direction)connected (outgoing, legacy audio)— Connected with legacy audio codec fallbackconnected (outgoing via PBX, legacy audio)— Connected via PBX with legacy audio fallbackpaging...— Broadcast paging startingpaging (N active)— Broadcast paging in progress to N panelsfailed— Call attempt failed
Discovering Network Panels (P2P Mode)
Retrieve panels automatically discovered via mDNS/broadcast on the local network subnet:
const panels = await window.appcontainer.sip.listPanels();
console.log('Discovered panels:', panels);
Response Example
[
{ "name": "Panel-Kitchen", "host": "192.168.1.50", "port": 5060 },
{ "name": "Panel-MasterBed", "host": "192.168.1.52", "port": 5060 }
]
🔔 Telephony Events
Register event handlers using window.appcontainer.sip.on(event, handler) and remove them with window.appcontainer.sip.off(event, handler).
// Incoming call alert
window.appcontainer.sip.on('incomingCall', ({ callId, callerName, isBroadcast, hasVideo }) => {
console.log(`Incoming call from ${callerName} (Video: ${hasVideo})`);
// Display custom UI modal with Answer / Reject / Ignore buttons
});
// Call connected
window.appcontainer.sip.on('callConnected', ({ callId, direction, hasVideo, viaPbx, legacy }) => {
console.log(`Call connected in direction: ${direction} (PBX: ${viaPbx})`);
});
// Call ended
window.appcontainer.sip.on('callEnded', ({ callId, reason }) => {
console.log(`Call ${callId} ended. Reason: ${reason}`);
// Reason can be: 'local' | 'remote' | 'remote_bye' | 'rejected' | 'timeout'
});
// Call failed
window.appcontainer.sip.on('callFailed', ({ reason, busy }) => {
console.warn(`Call failed: ${reason}, Callee busy: ${busy}`);
});
// PBX Registration status changes
window.appcontainer.sip.on('registrationChanged', ({ registered, lastError }) => {
console.log(`SIP Registration: ${registered ? 'ONLINE' : 'OFFLINE'}, Error: ${lastError}`);
});
// Panel discovery events (P2P)
window.appcontainer.sip.on('panelFound', ({ name, host, port }) => {
console.log(`Discovered intercom panel: ${name} (${host}:${port})`);
});
window.appcontainer.sip.on('panelLost', ({ name }) => {
console.log(`Intercom panel went offline: ${name}`);
});
// Broadcast paging count update
window.appcontainer.sip.on('pagingStatusChanged', ({ activeCount }) => {
console.log(`Active paging listeners: ${activeCount}`);
});
Unsubscribing from Events
const handleIncoming = (data) => {
console.log('Incoming call:', data);
};
// Subscribe
window.appcontainer.sip.on('incomingCall', handleIncoming);
// Unsubscribe when component unmounts
window.appcontainer.sip.off('incomingCall', handleIncoming);
📹 Native Video Windows
Web applications can display real-time video for active intercom calls by requesting native video window overlays positioned over placeholder HTML <div> elements or absolute screen coordinates. This follows the exact same overlay model as the Native Player (streamBridge).
Architecture & Key Behaviors
- Safe by Default: Video windows do not appear until explicitly requested via
showVideo(), preserving full backward compatibility for audio-only interfaces. - Automatic Lifecycle Teardown: Both remote and local video surfaces are destroyed automatically when the call terminates (
callEndedorcallFailed). Web applications re-request them (typically in acallConnectedhandler) during the next call, eliminating manual cleanup boilerplate. - Unified Media Engine: The native window transparently renders whichever video stream the call negotiates — WebRTC (panel-to-panel or PBX) or legacy H.264 video (such as 2N, DoorBird, or standard PBX extensions). The surface is positioned immediately and remains black/blank until the stream frame is decoded.
- Local & Remote Targets:
'remote': The far-end caller (e.g. outdoor door station or remote intercom panel).'local': The local device camera preview.
Showing Video (showVideo)
Request a video window overlay mapped to a placeholder DOM element:
window.appcontainer.sip.on('callConnected', ({ hasVideo }) => {
if (!hasVideo) return;
// Render far-end video over #remote-video-container
window.appcontainer.sip.showVideo({
target: 'remote',
divId: 'remote-video-container'
});
// Render local self-camera preview over #local-video-container
window.appcontainer.sip.showVideo({
target: 'local',
divId: 'local-video-container'
});
});
Fractional Screen Coordinates (rect)
Alternatively, specify fractional screen coordinates (x, y, w, h from 0.0 to 1.0) instead of a divId:
window.appcontainer.sip.showVideo({
target: 'remote',
rect: { x: 0.1, y: 0.1, w: 0.8, h: 0.8 }
});
Syncing Layout on Scroll & Resize (resizeVideo)
When layout shifts occur (e.g., page scrolling, window resizing, orientation changes, or DOM mutations), re-align the native video surface using ResizeObserver:
const remoteBox = document.getElementById('remote-video-container');
const observer = new ResizeObserver(() => {
window.appcontainer.sip.resizeVideo({
target: 'remote',
divId: 'remote-video-container'
});
});
observer.observe(remoteBox);
Hiding Video Windows (hideVideo)
Explicitly dismiss video overlays prior to call termination if desired:
// Hide only the local camera preview
window.appcontainer.sip.hideVideo({ target: 'local' });
// Hide only the remote caller video
window.appcontainer.sip.hideVideo({ target: 'remote' });
// Hide all video windows simultaneously (omit target or pass empty object)
window.appcontainer.sip.hideVideo({});
Tip: Explicit calls to
hideVideo()are rarely required because App Container automatically removes all video surfaces whencallEndedorcallFailedfires.
💡 Practical Web UI Example
Here is a full example illustrating how to integrate incoming call notifications, microphone mute toggle, video containers, and speed dial into an HTML5 web project:
<!DOCTYPE html>
<html>
<head>
<style>
#call-banner {
display: none;
position: fixed;
top: 20px;
right: 20px;
padding: 16px 24px;
background: #1e293b;
color: #fff;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0,0,0,0.4);
z-index: 10000;
font-family: sans-serif;
}
#video-wrapper {
display: none;
position: relative;
width: 480px;
height: 320px;
margin: 20px 0;
background: #0f172a;
border-radius: 12px;
overflow: hidden;
}
#remote-video-box {
width: 100%;
height: 100%;
}
#local-video-box {
position: absolute;
bottom: 12px;
right: 12px;
width: 120px;
height: 80px;
border: 2px solid #fff;
border-radius: 6px;
background: #334155;
}
.btn {
padding: 8px 16px;
margin: 4px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
}
.btn-green { background: #22c55e; color: white; }
.btn-red { background: #ef4444; color: white; }
.btn-gray { background: #64748b; color: white; }
.btn-blue { background: #3b82f6; color: white; }
</style>
</head>
<body>
<!-- Incoming Call Notification -->
<div id="call-banner">
<span id="caller-label">Incoming Call...</span>
<button class="btn btn-green" onclick="answerCall()">Answer</button>
<button class="btn btn-red" onclick="rejectCall()">Reject</button>
<button class="btn btn-gray" onclick="ignoreCall()">Mute Ring</button>
</div>
<!-- Video Window Placeholders -->
<div id="video-wrapper">
<div id="remote-video-box"></div>
<div id="local-video-box"></div>
</div>
<!-- Call Controls & Speed Dial -->
<div>
<button class="btn btn-green" onclick="callGate()">Call Gate (P2P)</button>
<button class="btn btn-gray" onclick="pageAllPanels()">Broadcast All</button>
<button class="btn btn-blue" id="mute-mic-btn" onclick="toggleMic()">Mute Mic</button>
<button class="btn btn-red" onclick="hangupCall()">Hang Up</button>
</div>
<script>
const banner = document.getElementById('call-banner');
const label = document.getElementById('caller-label');
const videoWrapper = document.getElementById('video-wrapper');
const muteMicBtn = document.getElementById('mute-mic-btn');
let isMicMuted = false;
// Register Intercom event listeners when running inside App Container
if (window.appcontainer?.sip) {
window.appcontainer.sip.on('incomingCall', ({ callerName, hasVideo }) => {
label.textContent = `Call from ${callerName || 'Door'} ${hasVideo ? '📹' : '📞'}`;
banner.style.display = 'block';
});
window.appcontainer.sip.on('callConnected', ({ hasVideo }) => {
banner.style.display = 'none';
if (hasVideo) {
videoWrapper.style.display = 'block';
window.appcontainer.sip.showVideo({ target: 'remote', divId: 'remote-video-box' });
window.appcontainer.sip.showVideo({ target: 'local', divId: 'local-video-box' });
}
});
window.appcontainer.sip.on('callEnded', () => {
banner.style.display = 'none';
videoWrapper.style.display = 'none';
});
window.appcontainer.sip.on('callFailed', () => {
banner.style.display = 'none';
videoWrapper.style.display = 'none';
});
// Keep native video overlay in sync on layout changes
new ResizeObserver(() => {
if (videoWrapper.style.display !== 'none') {
window.appcontainer.sip.resizeVideo({ target: 'remote', divId: 'remote-video-box' });
window.appcontainer.sip.resizeVideo({ target: 'local', divId: 'local-video-box' });
}
}).observe(videoWrapper);
}
async function answerCall() {
await window.appcontainer?.sip?.answer();
banner.style.display = 'none';
}
async function rejectCall() {
await window.appcontainer?.sip?.reject();
banner.style.display = 'none';
}
async function ignoreCall() {
await window.appcontainer?.sip?.ignore();
banner.style.display = 'none';
}
async function toggleMic() {
isMicMuted = !isMicMuted;
await window.appcontainer?.sip?.setMicMuted(isMicMuted);
muteMicBtn.textContent = isMicMuted ? 'Unmute Mic' : 'Mute Mic';
}
async function hangupCall() {
await window.appcontainer?.sip?.hangup();
}
async function callGate() {
await window.appcontainer?.sip?.placeCallToIp('192.168.1.50');
}
async function pageAllPanels() {
await window.appcontainer?.sip?.pageAll();
}
</script>
</body>
</html>