diff --git a/demo/demo-broadsoft.css b/demo/demo-broadsoft.css
new file mode 100644
index 000000000..0936965c9
--- /dev/null
+++ b/demo/demo-broadsoft.css
@@ -0,0 +1,307 @@
+/* BroadSoft Demo Styles */
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 20px;
+ background-color: #f5f5f5;
+ color: #333;
+}
+
+h2 {
+ color: #2c3e50;
+ border-bottom: 3px solid #3498db;
+ padding-bottom: 10px;
+}
+
+h3 {
+ color: #34495e;
+ margin-top: 0;
+}
+
+h4 {
+ color: #7f8c8d;
+ margin: 10px 0;
+}
+
+.description {
+ background-color: #ecf0f1;
+ padding: 15px;
+ border-radius: 5px;
+ margin-bottom: 20px;
+}
+
+.section {
+ background-color: white;
+ padding: 20px;
+ margin-bottom: 20px;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+/* Configuration Form */
+.config-form {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 15px;
+}
+
+.form-group {
+ display: flex;
+ flex-direction: column;
+}
+
+.form-group label {
+ font-weight: 600;
+ margin-bottom: 5px;
+ color: #555;
+}
+
+.form-group input {
+ padding: 8px 12px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-size: 14px;
+}
+
+.form-group input:focus {
+ outline: none;
+ border-color: #3498db;
+ box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
+}
+
+/* Buttons */
+.btn {
+ padding: 10px 20px;
+ border: none;
+ border-radius: 4px;
+ font-size: 14px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s;
+ margin-right: 10px;
+}
+
+.btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.btn-primary {
+ background-color: #3498db;
+ color: white;
+}
+
+.btn-primary:hover:not(:disabled) {
+ background-color: #2980b9;
+}
+
+.btn-secondary {
+ background-color: #95a5a6;
+ color: white;
+}
+
+.btn-secondary:hover:not(:disabled) {
+ background-color: #7f8c8d;
+}
+
+.btn-small {
+ padding: 6px 12px;
+ font-size: 12px;
+}
+
+/* Status */
+.status {
+ margin-top: 15px;
+ padding: 10px;
+ border-radius: 4px;
+ font-weight: 600;
+}
+
+.status-disconnected {
+ color: #e74c3c;
+}
+
+.status-connecting {
+ color: #f39c12;
+}
+
+.status-connected {
+ color: #27ae60;
+}
+
+/* Controls */
+.controls {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 15px;
+ align-items: center;
+}
+
+.checkbox-group {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.checkbox-group input[type="checkbox"] {
+ width: 18px;
+ height: 18px;
+ cursor: pointer;
+}
+
+.checkbox-group input[type="checkbox"]:disabled {
+ cursor: not-allowed;
+}
+
+.checkbox-group label {
+ font-weight: 500;
+ cursor: pointer;
+}
+
+/* Feature Panels */
+.feature-panel {
+ background-color: #f8f9fa;
+ border: 1px solid #dee2e6;
+ border-radius: 6px;
+ padding: 15px;
+ margin-bottom: 15px;
+}
+
+.feature-panel h4 {
+ margin-top: 0;
+ color: #495057;
+ border-bottom: 2px solid #dee2e6;
+ padding-bottom: 8px;
+}
+
+.feature-status {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 10px;
+ margin-top: 10px;
+}
+
+.feature-status > div {
+ padding: 8px;
+ background-color: white;
+ border-radius: 4px;
+ font-size: 14px;
+}
+
+.value {
+ font-weight: 600;
+ color: #2c3e50;
+}
+
+/* Event Log */
+.log-controls {
+ margin-bottom: 10px;
+}
+
+.event-log {
+ background-color: #1e1e1e;
+ color: #d4d4d4;
+ padding: 15px;
+ border-radius: 4px;
+ font-family: "Courier New", Courier, monospace;
+ font-size: 13px;
+ max-height: 400px;
+ overflow-y: auto;
+ line-height: 1.6;
+}
+
+.log-entry {
+ margin-bottom: 8px;
+ padding: 5px;
+ border-left: 3px solid transparent;
+}
+
+.log-entry.info {
+ border-left-color: #3498db;
+}
+
+.log-entry.success {
+ border-left-color: #27ae60;
+ background-color: rgba(39, 174, 96, 0.1);
+}
+
+.log-entry.warning {
+ border-left-color: #f39c12;
+ background-color: rgba(243, 156, 18, 0.1);
+}
+
+.log-entry.error {
+ border-left-color: #e74c3c;
+ background-color: rgba(231, 76, 60, 0.1);
+}
+
+.log-timestamp {
+ color: #95a5a6;
+ font-size: 11px;
+}
+
+.log-message {
+ margin-left: 10px;
+}
+
+/* Instructions */
+.instructions {
+ background-color: #fff9e6;
+ border-left: 4px solid #f39c12;
+}
+
+.test-scenario {
+ background-color: white;
+ padding: 15px;
+ margin-bottom: 15px;
+ border-radius: 4px;
+ border: 1px solid #e0e0e0;
+}
+
+.test-scenario h4 {
+ color: #2c3e50;
+ margin-top: 0;
+}
+
+.test-scenario ol {
+ margin: 10px 0;
+ padding-left: 25px;
+}
+
+.test-scenario pre {
+ background-color: #2c3e50;
+ color: #ecf0f1;
+ padding: 10px;
+ border-radius: 4px;
+ overflow-x: auto;
+ font-size: 12px;
+ margin: 8px 0;
+}
+
+/* Audio Element */
+audio {
+ width: 100%;
+ max-width: 500px;
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ body {
+ padding: 10px;
+ }
+
+ .config-form {
+ grid-template-columns: 1fr;
+ }
+
+ .feature-status {
+ grid-template-columns: 1fr;
+ }
+
+ .controls {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+}
diff --git a/demo/demo-broadsoft.html b/demo/demo-broadsoft.html
new file mode 100644
index 000000000..b4c6a10c4
--- /dev/null
+++ b/demo/demo-broadsoft.html
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+ SIP.js Demo - BroadSoft Extensions
+
+
+
+
+ < Index
+
+ Demo: BroadSoft Extensions Integration Test
+
+
+ This demo is designed to test BroadSoft Access-Side Extensions with FreeSWITCH:
+
+ - Auto-Answer - Receives calls with Call-Info header (answer-after parameter)
+ - Remote Control - Talk Events - Receives NOTIFY with Event: talk
+
+
+
+
+
+
+
2. Connection
+
+
+
+ Status: Disconnected
+
+
+
+
+
3. Audio
+
+
+
+
+
+
4. Manual Controls (for comparison)
+
+
+
+
+
+
+
+
+
+
+
+
5. BroadSoft Features Status
+
+
+
Auto-Answer
+
+
Enabled: -
+
Delay: -
+
Countdown: -
+
Status: Waiting for call...
+
+
+
+
+
Remote Control - Talk Events
+
+
Last Event: None
+
Microphone: Active
+
Auto-Apply: β Enabled
+
+
+
+
+
+
6. Event Log
+
+
+
+
+
+
+
+
Testing Instructions
+
+
Test 1: Auto-Answer
+
+ - Connect the SIP.js client above
+ - From FreeSWITCH, originate a call with Call-Info header:
+
originate {sip_h_Call-Info=<sip:${domain}>; answer-after=2}user/1000 &echo
+
+ - Observe: Call should auto-answer after 2 seconds
+
+
+
+
+
Test 2: Remote Control - Talk
+
+ - Establish a call (auto-answer or manual)
+ - From FreeSWITCH console, send talk event:
+
uuid_phone_event <uuid> talk
+
+ - Observe: Talk event is received and processed
+
+
+
+
+
+
+
+
diff --git a/demo/demo-broadsoft.ts b/demo/demo-broadsoft.ts
new file mode 100644
index 000000000..c044f6b35
--- /dev/null
+++ b/demo/demo-broadsoft.ts
@@ -0,0 +1,439 @@
+/* eslint-disable @typescript-eslint/no-use-before-define */
+/* eslint-disable no-console */
+/**
+ * BroadSoft Extensions Integration Test Demo
+ *
+ * This demo demonstrates the integration of BroadSoft Access-Side Extensions
+ * with FreeSWITCH, including:
+ * 1. Auto-Answer (Call-Info header with answer-after parameter)
+ * 2. Remote Control - Talk Events (mute/unmute via NOTIFY)
+ */
+
+import { Invitation } from "../lib/api/invitation.js";
+import { Notification } from "../lib/api/notification.js";
+import { Registerer } from "../lib/api/registerer.js";
+import { RegistererState } from "../lib/api/registerer-state.js";
+import { UserAgent } from "../lib/api/user-agent.js";
+import { UserAgentOptions } from "../lib/api/user-agent-options.js";
+import {
+ SessionDescriptionHandler,
+ defaultSessionDescriptionHandlerFactory
+} from "../lib/platform/web/session-description-handler/index.js";
+import * as BroadSoft from "../lib/api/broadsoft/index.js";
+
+// DOM Elements (will be initialized after DOM loads)
+let serverInput: HTMLInputElement;
+let userInput: HTMLInputElement;
+let passwordInput: HTMLInputElement;
+let domainInput: HTMLInputElement;
+
+let connectButton: HTMLButtonElement;
+let disconnectButton: HTMLButtonElement;
+let answerButton: HTMLButtonElement;
+let hangupButton: HTMLButtonElement;
+
+let connectionStatus: HTMLSpanElement;
+let remoteAudio: HTMLAudioElement;
+let localAudio: HTMLAudioElement;
+
+let muteCheckbox: HTMLInputElement;
+
+// BroadSoft Status Elements
+let autoAnswerEnabled: HTMLSpanElement;
+let autoAnswerDelay: HTMLSpanElement;
+let autoAnswerCountdown: HTMLSpanElement;
+let autoAnswerStatus: HTMLSpanElement;
+
+let talkEvent: HTMLSpanElement;
+let talkStatus: HTMLSpanElement;
+
+let eventLog: HTMLDivElement;
+let clearLogButton: HTMLButtonElement;
+
+// State
+let userAgent: UserAgent | undefined;
+let registerer: Registerer | undefined;
+let currentSession: Invitation | undefined;
+let autoAnswerTimer: number | undefined;
+
+// Initialize
+document.addEventListener("DOMContentLoaded", () => {
+ // Initialize DOM elements
+ serverInput = document.getElementById("server") as HTMLInputElement;
+ userInput = document.getElementById("user") as HTMLInputElement;
+ passwordInput = document.getElementById("password") as HTMLInputElement;
+ domainInput = document.getElementById("domain") as HTMLInputElement;
+
+ connectButton = document.getElementById("connect") as HTMLButtonElement;
+ disconnectButton = document.getElementById("disconnect") as HTMLButtonElement;
+ answerButton = document.getElementById("answer") as HTMLButtonElement;
+ hangupButton = document.getElementById("hangup") as HTMLButtonElement;
+
+ connectionStatus = document.getElementById("connection-status") as HTMLSpanElement;
+ remoteAudio = document.getElementById("remoteAudio") as HTMLAudioElement;
+ localAudio = document.getElementById("localAudio") as HTMLAudioElement;
+
+ muteCheckbox = document.getElementById("mute") as HTMLInputElement;
+
+ autoAnswerEnabled = document.getElementById("auto-answer-enabled") as HTMLSpanElement;
+ autoAnswerDelay = document.getElementById("auto-answer-delay") as HTMLSpanElement;
+ autoAnswerCountdown = document.getElementById("auto-answer-countdown") as HTMLSpanElement;
+ autoAnswerStatus = document.getElementById("auto-answer-status") as HTMLSpanElement;
+
+ talkEvent = document.getElementById("talk-event") as HTMLSpanElement;
+ talkStatus = document.getElementById("talk-status") as HTMLSpanElement;
+
+ eventLog = document.getElementById("event-log") as HTMLDivElement;
+ clearLogButton = document.getElementById("clear-log") as HTMLButtonElement;
+
+ // Setup event listeners
+ connectButton.addEventListener("click", connect);
+ disconnectButton.addEventListener("click", disconnect);
+ answerButton.addEventListener("click", answerCall);
+ hangupButton.addEventListener("click", hangupCall);
+ muteCheckbox.addEventListener("change", toggleMute);
+ clearLogButton.addEventListener("click", clearLog);
+
+ log("Demo loaded. Configure settings and click Connect.", "info");
+});
+
+// Logging
+function log(message: string, type: "info" | "success" | "warning" | "error" = "info"): void {
+ const timestamp = new Date().toLocaleTimeString();
+ const entry = document.createElement("div");
+ entry.className = `log-entry ${type}`;
+ entry.innerHTML = `[${timestamp}]${message}`;
+ eventLog.appendChild(entry);
+ eventLog.scrollTop = eventLog.scrollHeight;
+ console.log(`[${timestamp}] ${message}`);
+}
+
+function clearLog(): void {
+ eventLog.innerHTML = "";
+ log("Log cleared.", "info");
+}
+
+// Connection
+async function connect(): Promise {
+ const server = serverInput.value;
+ const user = userInput.value;
+ const password = passwordInput.value;
+ const domain = domainInput.value;
+
+ if (!server || !user || !password || !domain) {
+ log("Please fill in all configuration fields.", "error");
+ return;
+ }
+
+ log(`Connecting to ${server} as ${user}@${domain}...`, "info");
+ updateConnectionStatus("connecting");
+
+ const uri = UserAgent.makeURI(`sip:${user}@${domain}`);
+ if (!uri) {
+ log("Invalid SIP URI", "error");
+ updateConnectionStatus("disconnected");
+ return;
+ }
+
+ const options: UserAgentOptions = {
+ uri,
+ transportOptions: {
+ server
+ },
+ authorizationUsername: user,
+ authorizationPassword: password,
+ sessionDescriptionHandlerFactory: defaultSessionDescriptionHandlerFactory(),
+ delegate: {
+ onInvite: (invitation: Invitation) => {
+ handleIncomingCall(invitation);
+ }
+ }
+ };
+
+ try {
+ userAgent = new UserAgent(options);
+ userAgent.start().then(() => {
+ log("WebSocket connected, registering...", "info");
+ updateConnectionStatus("connecting");
+
+ // Create Registerer
+ registerer = new Registerer(userAgent as UserAgent);
+
+ // Listen for registration state changes
+ registerer.stateChange.addListener((state: RegistererState) => {
+ log(`Registration state: ${state}`, "info");
+ if (state === RegistererState.Registered) {
+ log("β Registered successfully! Ready to receive calls.", "success");
+ updateConnectionStatus("connected");
+ connectButton.disabled = true;
+ disconnectButton.disabled = false;
+ } else if (state === RegistererState.Unregistered) {
+ log("Unregistered", "warning");
+ } else if (state === RegistererState.Terminated) {
+ log("Registration terminated", "error");
+ updateConnectionStatus("disconnected");
+ }
+ });
+
+ // Send REGISTER
+ registerer.register().catch((error) => {
+ log(`Registration failed: ${error}`, "error");
+ updateConnectionStatus("disconnected");
+ });
+ });
+ } catch (error) {
+ log(`Connection failed: ${error}`, "error");
+ updateConnectionStatus("disconnected");
+ }
+}
+
+async function disconnect(): Promise {
+ // Unregister first
+ if (registerer && registerer.state === RegistererState.Registered) {
+ log("Unregistering...", "info");
+ try {
+ await registerer.unregister();
+ log("Unregistered successfully", "success");
+ } catch (error) {
+ log(`Failed to unregister: ${error}`, "warning");
+ }
+ }
+
+ // Then stop user agent
+ if (userAgent) {
+ log("Disconnecting...", "info");
+ await userAgent.stop();
+ userAgent = undefined;
+ registerer = undefined;
+ updateConnectionStatus("disconnected");
+ connectButton.disabled = false;
+ disconnectButton.disabled = true;
+ log("Disconnected.", "success");
+ }
+}
+
+function updateConnectionStatus(status: "disconnected" | "connecting" | "connected"): void {
+ connectionStatus.className = `status-${status}`;
+ connectionStatus.textContent = status.charAt(0).toUpperCase() + status.slice(1);
+}
+
+// Call Handling
+function handleIncomingCall(invitation: Invitation): void {
+ log("π Incoming call received", "info");
+ currentSession = invitation;
+
+ // Setup session delegate for remote control FIRST (before any NOTIFY arrives)
+ log("π§ Setting up session delegate for remote control", "info");
+ setupSessionDelegate(invitation);
+ log("β
Session delegate setup complete", "info");
+
+ // Check for auto-answer
+ const shouldAutoAns = BroadSoft.shouldAutoAnswer(invitation);
+ const delay = BroadSoft.getAutoAnswerDelay(invitation.request);
+
+ autoAnswerEnabled.textContent = shouldAutoAns ? "β Yes" : "β No";
+ autoAnswerDelay.textContent = delay !== undefined ? `${delay} seconds` : "N/A";
+
+ if (shouldAutoAns && delay !== undefined) {
+ log(`π€ Auto-answer enabled with ${delay}s delay`, "success");
+ autoAnswerStatus.textContent = "Auto-answering...";
+ startAutoAnswerCountdown(delay);
+
+ // Configure auto-answer options
+ const autoAnswerOptions: BroadSoft.AutoAnswerOptions = {
+ enabled: true,
+ onBeforeAutoAnswer: (delaySeconds) => {
+ log(`Auto-answer will trigger in ${delaySeconds} seconds`, "info");
+ },
+ onAfterAutoAnswer: () => {
+ log("β Call auto-answered!", "success");
+ autoAnswerStatus.textContent = "Answered automatically";
+ stopAutoAnswerCountdown();
+ }
+ };
+
+ // Handle auto-answer
+ BroadSoft.handleAutoAnswer(invitation, autoAnswerOptions);
+ } else {
+ log("Manual answer required. Click 'Answer Call' button.", "warning");
+ autoAnswerStatus.textContent = "Waiting for manual answer...";
+ answerButton.disabled = false;
+ }
+
+ // Setup session state listener
+ invitation.stateChange.addListener((state: string) => {
+ log(`Session state: ${state}`, "info");
+ if (state === "Established") {
+ onCallEstablished();
+ } else if (state === "Terminated") {
+ onCallTerminated();
+ }
+ });
+}
+
+function setupSessionDelegate(session: Invitation): void {
+ // Per BroadSoft spec, remote control NOTIFY automatically triggers SIP signaling:
+ // - Event: talk β Answers ringing call or resumes from hold (sends 200 OK or re-INVITE)
+ // These callbacks are for UI updates only
+ const remoteControlOptions: BroadSoft.RemoteControlOptions = {
+ enabled: true,
+ onTalkEvent: (action) => {
+ log(`π€ Remote Control - Talk Event: ${action}`, "warning");
+ talkEvent.textContent = action;
+ if (action === BroadSoft.TalkAction.Mute) {
+ talkStatus.textContent = "Muted (by remote)";
+ talkStatus.style.color = "#e74c3c";
+ muteCheckbox.checked = true;
+ } else {
+ talkStatus.textContent = "Active (call will be answered/resumed automatically)";
+ talkStatus.style.color = "#27ae60";
+ muteCheckbox.checked = false;
+ }
+ }
+ };
+
+ session.delegate = {
+ onNotify: (notification: Notification) => {
+ log("π¨ NOTIFY received", "info");
+
+ const isBroadSoft = BroadSoft.isBroadSoftNotification(notification);
+
+ if (isBroadSoft) {
+ log("β BroadSoft remote control NOTIFY detected", "success");
+ const eventType = BroadSoft.parseEventHeaderFromNotification(notification);
+ log(`Event type: ${eventType}`, "info");
+
+ BroadSoft.handleRemoteControlNotification(session, notification, remoteControlOptions)
+ .then(() => {
+ log("Remote control action applied", "success");
+ })
+ .catch((error) => {
+ log(`Error handling remote control: ${error}`, "error");
+ });
+
+ notification.accept();
+ } else {
+ log("Non-BroadSoft NOTIFY, accepting", "info");
+ notification.accept();
+ }
+ }
+ };
+}
+
+function startAutoAnswerCountdown(seconds: number): void {
+ let remaining = seconds;
+ autoAnswerCountdown.textContent = `${remaining}s`;
+
+ autoAnswerTimer = window.setInterval(() => {
+ remaining--;
+ if (remaining >= 0) {
+ autoAnswerCountdown.textContent = `${remaining}s`;
+ } else {
+ stopAutoAnswerCountdown();
+ }
+ }, 1000);
+}
+
+function stopAutoAnswerCountdown(): void {
+ if (autoAnswerTimer) {
+ clearInterval(autoAnswerTimer);
+ autoAnswerTimer = undefined;
+ }
+ autoAnswerCountdown.textContent = "-";
+}
+
+async function answerCall(): Promise {
+ if (currentSession && currentSession.state === "Initial") {
+ log("Answering call manually...", "info");
+ try {
+ await currentSession.accept();
+ log("β Call answered", "success");
+ answerButton.disabled = true;
+ } catch (error) {
+ log(`Failed to answer call: ${error}`, "error");
+ }
+ }
+}
+
+async function hangupCall(): Promise {
+ if (currentSession) {
+ log("Hanging up call...", "info");
+ try {
+ await currentSession.bye();
+ log("β Call hung up", "success");
+ } catch (error) {
+ log(`Failed to hang up: ${error}`, "error");
+ }
+ }
+}
+
+function onCallEstablished(): void {
+ log("β Call established", "success");
+ answerButton.disabled = true;
+ hangupButton.disabled = false;
+ muteCheckbox.disabled = false;
+
+ // Setup audio
+ if (currentSession && currentSession.sessionDescriptionHandler) {
+ const sdh = currentSession.sessionDescriptionHandler as SessionDescriptionHandler;
+
+ // Remote audio
+ const remoteStream = sdh.remoteMediaStream;
+ if (remoteStream) {
+ remoteAudio.srcObject = remoteStream;
+ remoteAudio.play().catch((e) => log(`Error playing audio: ${e}`, "error"));
+ }
+
+ // Local audio (muted for monitoring)
+ const localStream = sdh.localMediaStream;
+ if (localStream) {
+ localAudio.srcObject = localStream;
+ }
+ }
+}
+
+function onCallTerminated(): void {
+ log("Call terminated", "info");
+ currentSession = undefined;
+ answerButton.disabled = true;
+ hangupButton.disabled = true;
+ muteCheckbox.disabled = true;
+ muteCheckbox.checked = false;
+
+ // Reset BroadSoft status
+ autoAnswerEnabled.textContent = "-";
+ autoAnswerDelay.textContent = "-";
+ autoAnswerCountdown.textContent = "-";
+ autoAnswerStatus.textContent = "Waiting for call...";
+ talkEvent.textContent = "None";
+ talkStatus.textContent = "Active";
+ talkStatus.style.color = "#27ae60";
+
+ stopAutoAnswerCountdown();
+
+ // Clear audio
+ remoteAudio.srcObject = null;
+ localAudio.srcObject = null;
+}
+
+// Manual Controls
+async function toggleMute(): Promise {
+ if (currentSession && currentSession.sessionDescriptionHandler) {
+ const sdh = currentSession.sessionDescriptionHandler as SessionDescriptionHandler;
+ const localStream = sdh.localMediaStream;
+ if (localStream) {
+ const audioTracks = localStream.getAudioTracks();
+ if (muteCheckbox.checked) {
+ log("π€ Muting microphone (manual)", "info");
+ audioTracks.forEach((track) => (track.enabled = false));
+ talkStatus.textContent = "Muted (manual)";
+ talkStatus.style.color = "#f39c12";
+ } else {
+ log("π€ Unmuting microphone (manual)", "info");
+ audioTracks.forEach((track) => (track.enabled = true));
+ talkStatus.textContent = "Active";
+ talkStatus.style.color = "#27ae60";
+ }
+ }
+ }
+}
diff --git a/demo/index.html b/demo/index.html
index 8ce1d960a..f5ddbd4cc 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -47,6 +47,14 @@
Answering incoming call with data channel
+
+
+ - Auto-Answer with Call-Info header
+ - Remote Control - Talk Events (mute/unmute)
+ - Remote Control - Hold Events (hold/resume)
+ - Integration testing with FreeSWITCH
+
+