feat: add WebRTC connection monitoring tools

Add real-time WebRTC connection monitoring for testing video calling apps.
Monitors real RTCPeerConnection instances and collects connection statistics.

New tools:
- browser_start_webrtc_monitoring: Enable monitoring and stats collection
- browser_get_webrtc_connections: List connections with state filtering
- browser_get_webrtc_stats: Detailed stats (bitrate, packet loss, jitter, RTT)
- browser_stop_webrtc_monitoring: Stop stats polling, preserve data
- browser_clear_webrtc_data: Clear captured connection data

Implementation:
- Intercepts RTCPeerConnection constructor via page.addInitScript()
- Captures state changes via page.exposeFunction() callbacks
- Collects stats via page.evaluate() calling getStats()
- Tab-level storage with Context aggregation (same pattern as notifications)
- Configurable stats polling interval (default 1s)

Use cases:
- Test WebRTC call quality in real-time
- Monitor connection states and failures
- Analyze bandwidth, packet loss, frame rates
This commit is contained in:
Ryan Malloy 2026-01-16 20:39:44 -07:00
parent a7d10f71bd
commit 96641283c3
6 changed files with 722 additions and 2 deletions

View File

@ -24,7 +24,7 @@ import { EnvironmentIntrospector } from './environmentIntrospection.js';
import { RequestInterceptor, RequestInterceptorOptions } from './requestInterceptor.js';
import { ArtifactManagerRegistry } from './artifactManager.js';
import type { Tool, WebNotification } from './tools/tool.js';
import type { Tool, WebNotification, RTCConnectionData } from './tools/tool.js';
import type { FullConfig } from './config.js';
import type { BrowserContextFactory } from './browserContextFactory.js';
import type { InjectionConfig } from './tools/codeInjection.js';
@ -97,6 +97,9 @@ export class Context {
// Browser notifications storage
private _notifications: WebNotification[] = [];
// WebRTC connections storage
private _rtcConnections: RTCConnectionData[] = [];
constructor(tools: Tool[], config: FullConfig, browserContextFactory: BrowserContextFactory, environmentIntrospector?: EnvironmentIntrospector) {
this.tools = tools;
this.config = config;
@ -177,6 +180,28 @@ export class Context {
this._notifications.length = 0;
}
// WebRTC connection management methods
addRTCConnection(connection: RTCConnectionData): void {
this._rtcConnections.push(connection);
}
rtcConnections(): RTCConnectionData[] {
return this._rtcConnections;
}
getRTCConnection(id: string): RTCConnectionData | undefined {
return this._rtcConnections.find(c => c.id === id);
}
clearRTCConnections(): void {
this._rtcConnections.length = 0;
// Also clear from all tabs
for (const tab of this.tabs())
tab.clearRTCConnections();
}
async newTab(): Promise<Tab> {
const { browserContext } = await this._ensureBrowserContext();
const page = await browserContext.newPage();

View File

@ -21,7 +21,7 @@ import * as playwright from 'playwright';
import { callOnPageNoTrace, waitForCompletion } from './tools/utils.js';
import { logUnhandledError } from './log.js';
import { ManualPromise } from './manualPromise.js';
import { ModalState, WebNotification } from './tools/tool.js';
import { ModalState, WebNotification, RTCConnectionData, RTCStatsSnapshot } from './tools/tool.js';
import { outputFile } from './config.js';
import type { Context } from './context.js';
@ -49,6 +49,10 @@ export class Tab extends EventEmitter<TabEventsInterface> {
private _downloads: { download: playwright.Download, finished: boolean, outputFile: string }[] = [];
private _notifications: WebNotification[] = [];
private _notificationIdCounter = 0;
private _rtcConnections: RTCConnectionData[] = [];
private _rtcConnectionIdCounter = 0;
private _rtcStatsPollingInterval: NodeJS.Timeout | undefined;
private _rtcMonitoringEnabled = false;
constructor(context: Context, page: playwright.Page, onPageClose: (tab: Tab) => void) {
super();
@ -441,6 +445,317 @@ export class Tab extends EventEmitter<TabEventsInterface> {
}
}
async _initializeRTCMonitoring() {
try {
// Expose callback for new RTC connections
await this.page.exposeFunction('__playwright_rtcConnectionCreated', (data: { internalId: string }) => {
this._handleRTCConnectionCreated(data);
});
// Expose callback for state updates
await this.page.exposeFunction('__playwright_rtcConnectionUpdate', (data: {
id: string;
connectionState: RTCPeerConnectionState;
iceConnectionState: RTCIceConnectionState;
iceGatheringState: RTCIceGatheringState;
signalingState: RTCSignalingState;
}) => {
this._handleRTCConnectionUpdate(data);
});
// Inject RTCPeerConnection interceptor
await this.page.addInitScript(() => {
// Store original constructor
const OriginalRTCPeerConnection = window.RTCPeerConnection;
// Map to track connections by internal ID
const rtcConnections = new Map<string, RTCPeerConnection>();
let connectionCounter = 0;
// Intercepting RTCPeerConnection class
class InterceptedRTCPeerConnection extends OriginalRTCPeerConnection {
private _internalId: string;
constructor(configuration?: RTCConfiguration) {
super(configuration);
// Generate internal ID
this._internalId = `browser-rtc-${++connectionCounter}-${Date.now()}`;
rtcConnections.set(this._internalId, this);
// Report connection creation
try {
(window as any).__playwright_rtcConnectionCreated({
internalId: this._internalId,
});
} catch (e) {
// Ignore
}
// Report state changes
const reportState = () => {
try {
(window as any).__playwright_rtcConnectionUpdate({
id: this._internalId,
connectionState: this.connectionState,
iceConnectionState: this.iceConnectionState,
iceGatheringState: this.iceGatheringState,
signalingState: this.signalingState,
});
} catch (e) {
// Ignore
}
};
// Listen to all state change events
this.addEventListener('connectionstatechange', reportState);
this.addEventListener('iceconnectionstatechange', reportState);
this.addEventListener('icegatheringstatechange', reportState);
this.addEventListener('signalingstatechange', reportState);
// Report initial state
setTimeout(reportState, 0);
// Clean up on close
this.addEventListener('connectionstatechange', () => {
if (this.connectionState === 'closed')
rtcConnections.delete(this._internalId);
});
}
}
// Copy static properties and methods
Object.setPrototypeOf(InterceptedRTCPeerConnection, OriginalRTCPeerConnection);
Object.setPrototypeOf(InterceptedRTCPeerConnection.prototype, OriginalRTCPeerConnection.prototype);
// Replace global RTCPeerConnection
(window as any).RTCPeerConnection = InterceptedRTCPeerConnection;
// Store connections map globally for stats access
(window as any).__rtcConnections = rtcConnections;
});
this._rtcMonitoringEnabled = true;
} catch (error) {
logUnhandledError(error);
}
}
private _handleRTCConnectionCreated(data: { internalId: string }) {
const connection: RTCConnectionData & { _internalId: string } = {
id: `rtc-${++this._rtcConnectionIdCounter}-${Date.now()}`,
origin: this.page.url(),
timestamp: Date.now(),
connectionState: 'new',
iceConnectionState: 'new',
iceGatheringState: 'new',
signalingState: 'stable',
stateHistory: [],
_internalId: data.internalId,
};
this._rtcConnections.push(connection as any);
this.context.addRTCConnection(connection);
}
private _handleRTCConnectionUpdate(data: {
id: string;
connectionState: RTCPeerConnectionState;
iceConnectionState: RTCIceConnectionState;
iceGatheringState: RTCIceGatheringState;
signalingState: RTCSignalingState;
}) {
const connection = this._rtcConnections.find(c => (c as any)._internalId === data.id);
if (!connection) return;
connection.connectionState = data.connectionState;
connection.iceConnectionState = data.iceConnectionState;
connection.iceGatheringState = data.iceGatheringState;
connection.signalingState = data.signalingState;
connection.stateHistory.push({
timestamp: Date.now(),
connectionState: data.connectionState,
iceConnectionState: data.iceConnectionState,
});
// Limit history size
if (connection.stateHistory.length > 100)
connection.stateHistory.shift();
}
private async _requestRTCStats(internalId: string): Promise<void> {
try {
// Execute in page context to get stats
const rawStats = await this.page.evaluate(async (id) => {
const rtcConnections = (window as any).__rtcConnections;
const pc = rtcConnections?.get(id);
if (!pc) return null;
const stats = await pc.getStats();
const result: any = {};
stats.forEach((report: any) => {
if (!result[report.type])
result[report.type] = [];
// Convert to plain object
const obj: any = { id: report.id, timestamp: report.timestamp, type: report.type };
for (const key in report) {
if (typeof report[key] !== 'function')
obj[key] = report[key];
}
result[report.type].push(obj);
});
return result;
}, internalId);
if (!rawStats) return;
// Find our connection
const connection = this._rtcConnections.find(c => (c as any)._internalId === internalId);
if (!connection) return;
// Parse stats into our structured format
connection.lastStats = this._parseRTCStats(rawStats);
connection.lastStatsTimestamp = Date.now();
} catch (error) {
logUnhandledError(error);
}
}
private _parseRTCStats(rawStats: any): RTCStatsSnapshot {
const snapshot: RTCStatsSnapshot = {};
// Parse inbound-rtp for video
const inboundVideo = rawStats['inbound-rtp']?.find((r: any) => r.kind === 'video');
if (inboundVideo) {
const packetLossRate = inboundVideo.packetsReceived > 0
? (inboundVideo.packetsLost / inboundVideo.packetsReceived) * 100
: 0;
snapshot.inboundVideo = {
packetsReceived: inboundVideo.packetsReceived || 0,
packetsLost: inboundVideo.packetsLost || 0,
packetLossRate: Math.round(packetLossRate * 100) / 100,
jitter: (inboundVideo.jitter || 0) * 1000, // Convert to ms
bytesReceived: inboundVideo.bytesReceived || 0,
bitrate: 0, // Will be calculated from deltas in polling
framesPerSecond: inboundVideo.framesPerSecond,
framesDecoded: inboundVideo.framesDecoded,
frameWidth: inboundVideo.frameWidth,
frameHeight: inboundVideo.frameHeight,
freezeCount: inboundVideo.freezeCount,
totalFreezesDuration: inboundVideo.totalFreezesDuration,
};
}
// Parse inbound-rtp for audio
const inboundAudio = rawStats['inbound-rtp']?.find((r: any) => r.kind === 'audio');
if (inboundAudio) {
const packetLossRate = inboundAudio.packetsReceived > 0
? (inboundAudio.packetsLost / inboundAudio.packetsReceived) * 100
: 0;
snapshot.inboundAudio = {
packetsReceived: inboundAudio.packetsReceived || 0,
packetsLost: inboundAudio.packetsLost || 0,
packetLossRate: Math.round(packetLossRate * 100) / 100,
jitter: (inboundAudio.jitter || 0) * 1000,
bytesReceived: inboundAudio.bytesReceived || 0,
bitrate: 0,
audioLevel: inboundAudio.audioLevel,
concealedSamples: inboundAudio.concealedSamples,
};
}
// Parse outbound-rtp for video
const outboundVideo = rawStats['outbound-rtp']?.find((r: any) => r.kind === 'video');
if (outboundVideo) {
snapshot.outboundVideo = {
packetsSent: outboundVideo.packetsSent || 0,
bytesSent: outboundVideo.bytesSent || 0,
bitrate: 0,
framesPerSecond: outboundVideo.framesPerSecond,
framesEncoded: outboundVideo.framesEncoded,
frameWidth: outboundVideo.frameWidth,
frameHeight: outboundVideo.frameHeight,
qualityLimitationReason: outboundVideo.qualityLimitationReason,
};
}
// Parse outbound-rtp for audio
const outboundAudio = rawStats['outbound-rtp']?.find((r: any) => r.kind === 'audio');
if (outboundAudio) {
snapshot.outboundAudio = {
packetsSent: outboundAudio.packetsSent || 0,
bytesSent: outboundAudio.bytesSent || 0,
bitrate: 0,
};
}
// Parse candidate-pair (get the selected/active pair)
const candidatePairs = rawStats['candidate-pair'] || [];
const activePair = candidatePairs.find((p: any) => p.state === 'succeeded') || candidatePairs[0];
if (activePair) {
snapshot.candidatePair = {
state: activePair.state,
localCandidateType: activePair.localCandidateType || 'unknown',
remoteCandidateType: activePair.remoteCandidateType || 'unknown',
currentRoundTripTime: activePair.currentRoundTripTime,
availableOutgoingBitrate: activePair.availableOutgoingBitrate,
};
}
return snapshot;
}
enableRTCStatsPolling(intervalMs: number = 1000) {
if (this._rtcStatsPollingInterval)
clearInterval(this._rtcStatsPollingInterval);
this._rtcStatsPollingInterval = setInterval(async () => {
// Poll stats for all active connections
for (const connection of this._rtcConnections) {
if (connection.connectionState !== 'closed')
await this._requestRTCStats((connection as any)._internalId);
}
}, intervalMs);
}
disableRTCStatsPolling() {
if (this._rtcStatsPollingInterval) {
clearInterval(this._rtcStatsPollingInterval);
this._rtcStatsPollingInterval = undefined;
}
}
rtcConnections(): RTCConnectionData[] {
return this._rtcConnections;
}
getRTCConnection(id: string): RTCConnectionData | undefined {
return this._rtcConnections.find(c => c.id === id);
}
clearRTCConnections() {
this.disableRTCStatsPolling();
this._rtcConnections.length = 0;
this._rtcConnectionIdCounter = 0;
}
isRTCMonitoringEnabled(): boolean {
return this._rtcMonitoringEnabled;
}
private _handleNotificationShown(data: {
title: string;
body?: string;

View File

@ -36,6 +36,7 @@ import screenshot from './tools/screenshot.js';
import themeManagement from './tools/themeManagement.js';
import video from './tools/video.js';
import wait from './tools/wait.js';
import webrtc from './tools/webrtc.js';
import mouse from './tools/mouse.js';
import type { Tool } from './tools/tool.js';
@ -65,6 +66,7 @@ export const allTools: Tool<any>[] = [
...themeManagement,
...video,
...wait,
...webrtc,
];
export function filteredTools(config: FullConfig) {

View File

@ -55,6 +55,79 @@ export type NotificationModalState = {
notification: WebNotification;
};
export type RTCConnectionData = {
id: string;
origin: string;
timestamp: number;
connectionState: RTCPeerConnectionState;
iceConnectionState: RTCIceConnectionState;
iceGatheringState: RTCIceGatheringState;
signalingState: RTCSignalingState;
lastStats?: RTCStatsSnapshot;
lastStatsTimestamp?: number;
stateHistory: Array<{
timestamp: number;
connectionState: RTCPeerConnectionState;
iceConnectionState: RTCIceConnectionState;
}>;
};
export type RTCStatsSnapshot = {
inboundVideo?: {
packetsReceived: number;
packetsLost: number;
packetLossRate: number;
jitter: number;
bytesReceived: number;
bitrate: number;
framesPerSecond?: number;
framesDecoded?: number;
frameWidth?: number;
frameHeight?: number;
freezeCount?: number;
totalFreezesDuration?: number;
};
inboundAudio?: {
packetsReceived: number;
packetsLost: number;
packetLossRate: number;
jitter: number;
bytesReceived: number;
bitrate: number;
audioLevel?: number;
concealedSamples?: number;
};
outboundVideo?: {
packetsSent: number;
bytesSent: number;
bitrate: number;
framesPerSecond?: number;
framesEncoded?: number;
frameWidth?: number;
frameHeight?: number;
qualityLimitationReason?: string;
};
outboundAudio?: {
packetsSent: number;
bytesSent: number;
bitrate: number;
};
candidatePair?: {
state: string;
localCandidateType: string;
remoteCandidateType: string;
currentRoundTripTime?: number;
availableOutgoingBitrate?: number;
};
};
export type ModalState = FileUploadModalState | DialogModalState | NotificationModalState;
export type Tool<Input extends z.Schema = z.Schema> = {

300
src/tools/webrtc.ts Normal file
View File

@ -0,0 +1,300 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { z } from 'zod';
import { defineTool, defineTabTool } from './tool.js';
/**
* Start WebRTC monitoring by intercepting RTCPeerConnection.
*/
const startWebRTCMonitoring = defineTabTool({
capability: 'core',
schema: {
name: 'browser_start_webrtc_monitoring',
title: 'Start WebRTC monitoring',
description: 'Enable real-time WebRTC connection monitoring. Intercepts RTCPeerConnection API to track connection states and collect statistics. Required before using other WebRTC tools.',
inputSchema: z.object({
statsPollingInterval: z.number().optional().describe('Stats collection interval in milliseconds (default: 1000ms). Lower values give more frequent updates but use more CPU.'),
}),
type: 'destructive',
},
handle: async (tab, params, response) => {
if (tab.isRTCMonitoringEnabled()) {
response.addResult('⚠️ WebRTC monitoring is already enabled for this tab.');
return;
}
await tab._initializeRTCMonitoring();
const interval = params.statsPollingInterval || 1000;
tab.enableRTCStatsPolling(interval);
response.addResult(`✅ WebRTC monitoring enabled
Stats polling interval: ${interval}ms
All new RTCPeerConnection instances will be monitored
Use browser_get_webrtc_connections to view connections`);
},
});
/**
* Get WebRTC connections with state filtering.
*/
const getWebRTCConnections = defineTool({
capability: 'core',
schema: {
name: 'browser_get_webrtc_connections',
title: 'Get WebRTC connections',
description: 'List all WebRTC connections captured during this session. Shows connection states, ICE states, and origin. Use browser_get_webrtc_stats for detailed statistics.',
inputSchema: z.object({
connectionState: z.enum(['new', 'connecting', 'connected', 'disconnected', 'failed', 'closed']).optional()
.describe('Filter by connection state'),
iceConnectionState: z.enum(['new', 'checking', 'connected', 'completed', 'failed', 'disconnected', 'closed']).optional()
.describe('Filter by ICE connection state'),
origin: z.string().optional().describe('Filter by origin URL'),
}),
type: 'readOnly',
},
handle: async (context, params, response) => {
let connections = context.rtcConnections();
// Apply filters
if (params.connectionState)
connections = connections.filter(c => c.connectionState === params.connectionState);
if (params.iceConnectionState)
connections = connections.filter(c => c.iceConnectionState === params.iceConnectionState);
if (params.origin) {
const originFilter = params.origin;
connections = connections.filter(c => c.origin.includes(originFilter));
}
if (connections.length === 0) {
response.addResult('No WebRTC connections found. Use browser_start_webrtc_monitoring to enable monitoring.');
return;
}
const result = ['### WebRTC Connections', ''];
for (const conn of connections) {
const age = Math.round((Date.now() - conn.timestamp) / 1000);
const hostname = new URL(conn.origin).hostname;
result.push(`**Connection ${conn.id}**`);
result.push(` • Origin: ${hostname}`);
result.push(` • Age: ${age}s`);
result.push(` • Connection State: \`${conn.connectionState}\``);
result.push(` • ICE Connection: \`${conn.iceConnectionState}\``);
result.push(` • ICE Gathering: \`${conn.iceGatheringState}\``);
result.push(` • Signaling: \`${conn.signalingState}\``);
if (conn.lastStats && conn.lastStatsTimestamp) {
const statsAge = Math.round((Date.now() - conn.lastStatsTimestamp) / 1000);
result.push(` • Last Stats: ${statsAge}s ago`);
}
result.push('');
}
response.addResult(result.join('\n'));
},
});
/**
* Get detailed WebRTC stats for specific connections.
*/
const getWebRTCStats = defineTool({
capability: 'core',
schema: {
name: 'browser_get_webrtc_stats',
title: 'Get WebRTC statistics',
description: 'Get detailed real-time statistics for WebRTC connections. Includes bitrate, packet loss, jitter, RTT, frames per second, and quality metrics. Essential for diagnosing call quality issues.',
inputSchema: z.object({
connectionId: z.string().optional().describe('Specific connection ID (from browser_get_webrtc_connections). If omitted, shows stats for all connections.'),
includeRaw: z.boolean().optional().describe('Include raw stats data for debugging (default: false)'),
}),
type: 'readOnly',
},
handle: async (context, params, response) => {
let connections = context.rtcConnections();
if (params.connectionId) {
const conn = context.getRTCConnection(params.connectionId);
if (!conn)
throw new Error(`Connection "${params.connectionId}" not found. Use browser_get_webrtc_connections to list connections.`);
connections = [conn];
}
if (connections.length === 0) {
response.addResult('No WebRTC connections found.');
return;
}
const result = ['### WebRTC Statistics', ''];
for (const conn of connections) {
result.push(`## Connection ${conn.id}`);
result.push(`Origin: ${new URL(conn.origin).hostname}`);
result.push('');
if (!conn.lastStats) {
result.push('_No statistics collected yet. Stats polling may not be enabled._');
result.push('');
continue;
}
const stats = conn.lastStats;
// Inbound Video
if (stats.inboundVideo) {
result.push('**Inbound Video**');
result.push(` • Bitrate: ${stats.inboundVideo.bitrate.toFixed(2)} Mbps`);
result.push(` • Packet Loss: ${stats.inboundVideo.packetLossRate.toFixed(2)}%`);
result.push(` • Jitter: ${stats.inboundVideo.jitter.toFixed(2)} ms`);
if (stats.inboundVideo.framesPerSecond)
result.push(` • FPS: ${stats.inboundVideo.framesPerSecond}`);
if (stats.inboundVideo.frameWidth && stats.inboundVideo.frameHeight)
result.push(` • Resolution: ${stats.inboundVideo.frameWidth}x${stats.inboundVideo.frameHeight}`);
if (stats.inboundVideo.freezeCount)
result.push(` • Freezes: ${stats.inboundVideo.freezeCount} (${stats.inboundVideo.totalFreezesDuration?.toFixed(1)}s total)`);
result.push('');
}
// Inbound Audio
if (stats.inboundAudio) {
result.push('**Inbound Audio**');
result.push(` • Bitrate: ${stats.inboundAudio.bitrate.toFixed(2)} Mbps`);
result.push(` • Packet Loss: ${stats.inboundAudio.packetLossRate.toFixed(2)}%`);
result.push(` • Jitter: ${stats.inboundAudio.jitter.toFixed(2)} ms`);
if (stats.inboundAudio.audioLevel !== undefined)
result.push(` • Audio Level: ${(stats.inboundAudio.audioLevel * 100).toFixed(1)}%`);
result.push('');
}
// Outbound Video
if (stats.outboundVideo) {
result.push('**Outbound Video**');
result.push(` • Bitrate: ${stats.outboundVideo.bitrate.toFixed(2)} Mbps`);
if (stats.outboundVideo.framesPerSecond)
result.push(` • FPS: ${stats.outboundVideo.framesPerSecond}`);
if (stats.outboundVideo.frameWidth && stats.outboundVideo.frameHeight)
result.push(` • Resolution: ${stats.outboundVideo.frameWidth}x${stats.outboundVideo.frameHeight}`);
if (stats.outboundVideo.qualityLimitationReason && stats.outboundVideo.qualityLimitationReason !== 'none')
result.push(` • ⚠️ Quality Limited By: ${stats.outboundVideo.qualityLimitationReason}`);
result.push('');
}
// Outbound Audio
if (stats.outboundAudio) {
result.push('**Outbound Audio**');
result.push(` • Bitrate: ${stats.outboundAudio.bitrate.toFixed(2)} Mbps`);
result.push('');
}
// Candidate Pair
if (stats.candidatePair) {
result.push('**Connection Info**');
result.push(` • ICE State: ${stats.candidatePair.state}`);
result.push(` • Local Candidate: ${stats.candidatePair.localCandidateType}`);
result.push(` • Remote Candidate: ${stats.candidatePair.remoteCandidateType}`);
if (stats.candidatePair.currentRoundTripTime !== undefined)
result.push(` • RTT: ${(stats.candidatePair.currentRoundTripTime * 1000).toFixed(2)} ms`);
if (stats.candidatePair.availableOutgoingBitrate)
result.push(` • Available Bandwidth: ${(stats.candidatePair.availableOutgoingBitrate / 1000000).toFixed(2)} Mbps`);
result.push('');
}
}
response.addResult(result.join('\n'));
},
});
/**
* Stop WebRTC monitoring and stats polling.
*/
const stopWebRTCMonitoring = defineTabTool({
capability: 'core',
schema: {
name: 'browser_stop_webrtc_monitoring',
title: 'Stop WebRTC monitoring',
description: 'Stop collecting WebRTC statistics. Captured connection data is preserved and can still be queried. Use browser_clear_webrtc_data to also clear historical data.',
inputSchema: z.object({}),
type: 'destructive',
},
handle: async (tab, _params, response) => {
if (!tab.isRTCMonitoringEnabled()) {
response.addResult('WebRTC monitoring is not currently enabled.');
return;
}
tab.disableRTCStatsPolling();
const count = tab.rtcConnections().length;
response.addResult(`✅ WebRTC monitoring stopped
Stats polling disabled
${count} connection(s) preserved in history
Use browser_clear_webrtc_data to clear history`);
},
});
/**
* Clear all WebRTC data.
*/
const clearWebRTCData = defineTool({
capability: 'core',
schema: {
name: 'browser_clear_webrtc_data',
title: 'Clear WebRTC data',
description: 'Clear all captured WebRTC connection data and statistics from session history. Also stops monitoring if active.',
inputSchema: z.object({}),
type: 'destructive',
},
handle: async (context, _params, response) => {
const count = context.rtcConnections().length;
context.clearRTCConnections();
response.addResult(`✅ Cleared ${count} WebRTC connection(s) from history.`);
},
});
export default [
startWebRTCMonitoring,
getWebRTCConnections,
getWebRTCStats,
stopWebRTCMonitoring,
clearWebRTCData,
];

View File

@ -22,6 +22,7 @@ test('test snapshot tool list', async ({ client }) => {
'browser_clear_device_motion',
'browser_clear_device_orientation',
'browser_clear_injections',
'browser_clear_webrtc_data',
'browser_clear_notifications',
'browser_clear_permissions',
'browser_clear_requests',
@ -43,6 +44,8 @@ test('test snapshot tool list', async ({ client }) => {
'browser_file_upload',
'browser_get_artifact_paths',
'browser_get_requests',
'browser_get_webrtc_connections',
'browser_get_webrtc_stats',
'browser_grant_permissions',
'browser_handle_dialog',
'browser_handle_notification',
@ -80,8 +83,10 @@ test('test snapshot tool list', async ({ client }) => {
'browser_snapshot',
'browser_start_recording',
'browser_start_request_monitoring',
'browser_start_webrtc_monitoring',
'browser_status',
'browser_stop_recording',
'browser_stop_webrtc_monitoring',
'browser_tab_close',
'browser_tab_list',
'browser_tab_new',