);
+ await new Promise((resolve) => setTimeout(resolve, 1500));
+ await collectBrowserState(traceId);
} catch (error) {
- console.error('❌ INITIALIZE function failed:', error);
- console.error('❌ Error details:', {
+ logTrace(traceId, 'extension_initialize_failed', {
name: (error as Error).name,
message: (error as Error).message,
stack: (error as Error).stack
@@ -902,16 +1008,29 @@ async function handleStartStream(data: { url: string; peerId: string }): Promise
startConnectionMonitoring();
}
+ logTrace(traceId, 'start_stream_response_sent', {
+ srcPeerId,
+ monitoringActive: !!connectionCheckInterval,
+ });
return jsonResponse({
status: 'success',
+ traceId,
srcPeerId,
browserWSEndpoint: browser.wsEndpoint(),
monitoringActive: !!connectionCheckInterval
+ }, {
+ headers: buildTraceHeaders(traceId),
});
} catch (err: any) {
- console.error('handleStartStream error:', err);
+ logTrace(traceId, 'start_stream_error', {
+ message: err.message,
+ stack: err.stack,
+ });
// Don't close browser on error - let monitoring handle lifecycle
- return jsonResponse({ status: 'error', message: err.message }, { status: 500 });
+ return jsonResponse(
+ { status: 'error', message: err.message, traceId },
+ { status: 500, headers: buildTraceHeaders(traceId) }
+ );
}
}
@@ -919,12 +1038,18 @@ async function handleStartStream(data: { url: string; peerId: string }): Promise
const server = http.createServer(async (req: any, res: any) => {
const parsedUrl = url.parse(req.url || '', true);
const { pathname } = parsedUrl;
+ const traceId = req.headers['x-stream-trace-id'] || crypto.randomUUID();
try {
+ logTrace(traceId, 'http_request_received', {
+ method: req.method,
+ pathname,
+ });
// Set CORS headers
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, x-stream-trace-id');
+ res.setHeader('x-stream-trace-id', traceId);
// Handle CORS preflight requests
if (req.method === 'OPTIONS') {
@@ -941,6 +1066,8 @@ const server = http.createServer(async (req: any, res: any) => {
response = await handlePing();
} else if (pathname === '/test-puppeteer') {
response = await handleTest();
+ } else if (pathname === '/debug-state') {
+ response = await handleDebugState(traceId);
} else if (pathname === '/start-stream' && req.method === 'POST') {
// Parse request body for POST requests
let body = '';
@@ -952,10 +1079,10 @@ const server = http.createServer(async (req: any, res: any) => {
req.on('end', resolve);
});
- const data = JSON.parse(body);
- response = await handleStartStream(data);
+ const data = parseStartStreamRequest(JSON.parse(body));
+ response = await handleStartStream(data, traceId);
} else {
- response = new Response('Not Found', { status: 404 });
+ response = new Response(`Not Found: ${req.method} ${req.url} (parsed path: ${pathname})`, { status: 404 });
}
// Convert Response object to Node.js response
@@ -968,6 +1095,18 @@ const server = http.createServer(async (req: any, res: any) => {
res.end(responseText);
} catch (err: any) {
+ if (err instanceof SyntaxError || err?.message?.includes('must be')) {
+ res.writeHead(400, {
+ 'Content-Type': 'application/json',
+ 'Access-Control-Allow-Origin': '*',
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
+ 'Access-Control-Allow-Headers': 'Content-Type, x-stream-trace-id',
+ 'x-stream-trace-id': req.headers['x-stream-trace-id'] || '',
+ });
+ res.end(JSON.stringify({ status: 'error', message: err.message }));
+ return;
+ }
+
console.error('Unhandled error:', err);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: err.message }));
@@ -975,8 +1114,8 @@ const server = http.createServer(async (req: any, res: any) => {
});
const PORT = 8080;
-server.listen(PORT, () => {
- console.log(`Container server running at http://localhost:${PORT}`);
+server.listen(PORT, '0.0.0.0', () => {
+ console.log(`Container server running at http://0.0.0.0:${PORT}`);
});
// Graceful shutdown on process termination
@@ -990,4 +1129,4 @@ process.on('SIGINT', async () => {
console.log('Received SIGINT, shutting down gracefully...');
await shutdownBrowser();
process.exit(0);
-});
\ No newline at end of file
+});
diff --git a/examples/bun-stream-server/package.json b/examples/bun-stream-server/package.json
index c75076c..a7a4f8f 100644
--- a/examples/bun-stream-server/package.json
+++ b/examples/bun-stream-server/package.json
@@ -5,7 +5,8 @@
"scripts": {
"deploy": "wrangler deploy",
"dev": "wrangler dev",
- "start": "wrangler dev"
+ "start": "wrangler dev",
+ "test": "npx --yes vitest@2.1.9 run test/index.test.ts"
},
"devDependencies": {
"@types/bun": "^1.2.21",
diff --git a/examples/bun-stream-server/receiver.html b/examples/bun-stream-server/receiver.html
index d7ec8dd..77c091e 100644
--- a/examples/bun-stream-server/receiver.html
+++ b/examples/bun-stream-server/receiver.html
@@ -172,6 +172,11 @@ How to use:
+
+
+
+
+
@@ -209,6 +214,9 @@
How to use:
let currentConnection = null;
let receivedCall = null;
let isListening = false;
+ let callHandlersBound = false;
+ let currentTraceId = null;
+ const sessionId = 'session-' + Math.random().toString(36).substring(2, 15);
const urlInput = document.getElementById('urlInput');
const receiverIdDisplay = document.getElementById('receiverIdDisplay');
@@ -229,6 +237,67 @@ How to use:
status.textContent = message;
status.className = `status ${type}`;
}
+
+ async function logPeerConnectionStats(call, label = 'rtc') {
+ const pc = call && (call.peerConnection || call._negotiator?._pc || null);
+ if (!pc || typeof pc.getStats !== 'function') {
+ console.log(`📡 ${label}: peer connection stats unavailable`);
+ return;
+ }
+
+ try {
+ const stats = await pc.getStats();
+ const summary = {
+ connectionState: pc.connectionState,
+ iceConnectionState: pc.iceConnectionState,
+ iceGatheringState: pc.iceGatheringState,
+ signalingState: pc.signalingState,
+ inboundVideo: null,
+ inboundAudio: null,
+ candidatePair: null
+ };
+
+ stats.forEach((report) => {
+ if (report.type === 'inbound-rtp' && report.kind === 'video') {
+ summary.inboundVideo = {
+ bytesReceived: report.bytesReceived,
+ framesDecoded: report.framesDecoded,
+ frameWidth: report.frameWidth,
+ frameHeight: report.frameHeight,
+ framesPerSecond: report.framesPerSecond,
+ packetsLost: report.packetsLost
+ };
+ }
+
+ if (report.type === 'inbound-rtp' && report.kind === 'audio') {
+ summary.inboundAudio = {
+ bytesReceived: report.bytesReceived,
+ packetsLost: report.packetsLost
+ };
+ }
+
+ if (report.type === 'candidate-pair' && report.nominated && report.state === 'succeeded') {
+ summary.candidatePair = {
+ currentRoundTripTime: report.currentRoundTripTime,
+ availableIncomingBitrate: report.availableIncomingBitrate,
+ bytesReceived: report.bytesReceived,
+ bytesSent: report.bytesSent
+ };
+ }
+ });
+
+ console.log(`📡 ${label} stats:`, summary);
+ } catch (error) {
+ console.error(`📡 ${label} stats failed:`, error);
+ }
+ }
+
+ function createTraceId() {
+ if (globalThis.crypto && typeof globalThis.crypto.randomUUID === 'function') {
+ return globalThis.crypto.randomUUID();
+ }
+ return 'trace-' + Math.random().toString(36).slice(2) + '-' + Date.now();
+ }
async function startStreamAndListen() {
const url = urlInput.value.trim();
@@ -247,10 +316,36 @@ How to use:
startListeningBtn.disabled = true;
try {
- // Step 1: Start listening first
+ cleanup();
+
+ // Step 1: Load ICE configuration before creating either peer.
+ updateStatus('📡 Requesting stream from container server...', 'connecting');
+
+ const serverUrl = document.getElementById('serverUrlInput').value.trim();
+ const endpoint = serverUrl.endsWith('/') ? `${serverUrl}start-stream` : `${serverUrl}/start-stream`;
+ const iceServersEndpoint = serverUrl.endsWith('/') ? `${serverUrl}ice-servers` : `${serverUrl}/ice-servers`;
+ currentTraceId = createTraceId();
+ console.log('🧭 Stream trace ID:', currentTraceId);
+
+ const iceResponse = await fetch(iceServersEndpoint, {
+ method: 'GET',
+ headers: {
+ 'x-stream-trace-id': currentTraceId,
+ 'x-stream-session-id': sessionId
+ }
+ });
+ const icePayload = await iceResponse.json();
+ if (!iceResponse.ok) {
+ throw new Error((icePayload.error || 'Failed to load ICE servers') + ` (trace ${icePayload.traceId || currentTraceId})`);
+ }
+ const iceServers = Array.isArray(icePayload.iceServers) ? icePayload.iceServers : [];
+ console.log('🧊 ICE servers loaded:', iceServers);
+
+ // Step 2: Start listening with the fetched ICE servers.
peer = new Peer(receiverId, {
+ debug: 3,
config: {
- debug: 3
+ iceServers
}
});
@@ -268,29 +363,36 @@ How to use:
});
});
- // Step 2: Start the stream on the container server
- updateStatus('📡 Requesting stream from container server...', 'connecting');
+ // Step 3: Start listening for the incoming call before
+ // the container finishes INITIALIZE and dials our peer.
+ setupCallHandlers();
- const streamResponse = await fetch('http://localhost:8080/start-stream', {
+ // Step 4: Start the stream on the container server.
+ const streamResponse = await fetch(endpoint, {
method: 'POST',
- headers: { 'Content-Type': 'application/json' },
+ headers: {
+ 'Content-Type': 'application/json',
+ 'x-stream-trace-id': currentTraceId,
+ 'x-stream-session-id': sessionId
+ },
body: JSON.stringify({
url: url,
- peerId: receiverId
+ peerId: receiverId,
+ iceServers
})
});
const result = await streamResponse.json();
+ const responseTraceId = streamResponse.headers.get('x-stream-trace-id') || result.traceId || currentTraceId;
+ console.log('🧭 Stream response trace ID:', responseTraceId);
if (result.status !== 'success') {
- throw new Error(result.message || 'Failed to start stream');
+ throw new Error((result.message || 'Failed to start stream') + ` (trace ${responseTraceId})`);
}
- console.log('✅ Stream started:', result.srcPeerId);
- updateStatus('🎯 Stream started! Waiting for connection...', 'connecting');
-
- // Step 3: Set up call handling (the container will call us)
- setupCallHandlers();
+ console.log('✅ Stream started:', result.srcPeerId, 'trace:', responseTraceId);
+ peerIdInput.value = result.srcPeerId || '';
+ updateStatus(`🎯 Stream started! Waiting for connection... Trace: ${responseTraceId}`, 'connecting');
// Enable other controls
connectBtn.disabled = false;
@@ -303,9 +405,30 @@ How to use:
}
function setupCallHandlers() {
+ if (!peer || callHandlersBound) {
+ return;
+ }
+
+ callHandlersBound = true;
+
peer.on('call', (call) => {
console.log('📞 Receiving call from:', call.peer);
updateStatus('📡 Receiving stream from: ' + call.peer, 'connecting');
+ const peerConnection = call.peerConnection || call._negotiator?._pc || null;
+ if (peerConnection) {
+ console.log('📡 Peer connection object detected');
+ peerConnection.onconnectionstatechange = () => {
+ console.log('📡 connectionState:', peerConnection.connectionState);
+ };
+ peerConnection.oniceconnectionstatechange = () => {
+ console.log('📡 iceConnectionState:', peerConnection.iceConnectionState);
+ };
+ peerConnection.onicegatheringstatechange = () => {
+ console.log('📡 iceGatheringState:', peerConnection.iceGatheringState);
+ };
+ } else {
+ console.log('📡 Peer connection object not available on call');
+ }
// Answer the call without providing our own stream
call.answer();
@@ -447,7 +570,12 @@ How to use:
console.log(' - videoHeight:', remoteVideo.videoHeight);
console.log(' - networkState:', remoteVideo.networkState);
console.log(' - error:', remoteVideo.error);
+ logPeerConnectionStats(call, '2s');
}, 2000);
+
+ setTimeout(() => {
+ logPeerConnectionStats(call, '5s');
+ }, 5000);
disconnectBtn.disabled = false;
});
@@ -477,7 +605,9 @@ How to use:
isListening = true;
// Create our peer with the receiver ID
- peer = new Peer(receiverId);
+ peer = new Peer(receiverId, {
+ debug: 3
+ });
peer.on('open', (id) => {
console.log('Receiver peer opened with ID:', id);
@@ -584,6 +714,9 @@ How to use:
peer = null;
}
+ callHandlersBound = false;
+ currentTraceId = null;
+
if (remoteVideo.srcObject) {
remoteVideo.srcObject.getTracks().forEach(track => track.stop());
remoteVideo.srcObject = null;
@@ -625,7 +758,8 @@ How to use:
console.log('🎥 Stream Kit Receiver ready');
console.log('📱 Receiver ID:', receiverId);
+ console.log('🪪 Session ID:', sessionId);
console.log('💡 Enter a URL and click "Start Stream & Listen" to begin');