fixed tests
This commit is contained in:
335
tests/sfu/SFUDemoIntegration.test.ts
Normal file
335
tests/sfu/SFUDemoIntegration.test.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
import { test, expect, describe } from "bun:test";
|
||||
|
||||
// Mock WebSocket for testing SFU Demo Server functionality
|
||||
class MockWebSocket {
|
||||
public readyState = WebSocket.OPEN;
|
||||
public url: string;
|
||||
public sentMessages: string[] = [];
|
||||
|
||||
public onopen: (() => void) | null = null;
|
||||
public onclose: (() => void) | null = null;
|
||||
public onmessage: ((event: { data: string }) => void) | null = null;
|
||||
public onerror: ((error: any) => void) | null = null;
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
setTimeout(() => {
|
||||
if (this.onopen) {
|
||||
this.onopen();
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
|
||||
send(data: string) {
|
||||
if (this.readyState === WebSocket.OPEN) {
|
||||
this.sentMessages.push(data);
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
this.readyState = WebSocket.CLOSED;
|
||||
if (this.onclose) {
|
||||
this.onclose();
|
||||
}
|
||||
}
|
||||
|
||||
simulateMessage(data: any) {
|
||||
if (this.onmessage) {
|
||||
this.onmessage({ data: JSON.stringify(data) });
|
||||
}
|
||||
}
|
||||
|
||||
getLastMessage() {
|
||||
if (this.sentMessages.length === 0) return null;
|
||||
return JSON.parse(this.sentMessages[this.sentMessages.length - 1]);
|
||||
}
|
||||
|
||||
getAllMessages() {
|
||||
return this.sentMessages.map(msg => JSON.parse(msg));
|
||||
}
|
||||
}
|
||||
|
||||
describe("SFU Demo Server Integration", () => {
|
||||
test("should simulate SFU publisher connection flow", () => {
|
||||
const publisherWs = new MockWebSocket("ws://localhost:3001?role=publisher");
|
||||
|
||||
// Simulate server responses for publisher flow
|
||||
const messages: any[] = [];
|
||||
|
||||
publisherWs.onmessage = (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
messages.push(message);
|
||||
|
||||
// Simulate server responses
|
||||
if (message.type === 'join') {
|
||||
expect(message.data.role).toBe('publisher');
|
||||
expect(typeof message.data.clientId).toBe('string');
|
||||
}
|
||||
};
|
||||
|
||||
// Simulate join message from server
|
||||
publisherWs.simulateMessage({
|
||||
type: 'join',
|
||||
data: { clientId: 'publisher-123', role: 'publisher' }
|
||||
});
|
||||
|
||||
expect(messages).toHaveLength(1);
|
||||
expect(messages[0].type).toBe('join');
|
||||
expect(messages[0].data.role).toBe('publisher');
|
||||
});
|
||||
|
||||
test("should simulate SFU subscriber connection flow", () => {
|
||||
const subscriberWs = new MockWebSocket("ws://localhost:3001?role=subscriber");
|
||||
|
||||
const messages: any[] = [];
|
||||
|
||||
subscriberWs.onmessage = (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
messages.push(message);
|
||||
|
||||
if (message.type === 'join') {
|
||||
expect(message.data.role).toBe('subscriber');
|
||||
expect(typeof message.data.clientId).toBe('string');
|
||||
}
|
||||
};
|
||||
|
||||
// Simulate join message from server
|
||||
subscriberWs.simulateMessage({
|
||||
type: 'join',
|
||||
data: { clientId: 'subscriber-123', role: 'subscriber' }
|
||||
});
|
||||
|
||||
expect(messages).toHaveLength(1);
|
||||
expect(messages[0].type).toBe('join');
|
||||
expect(messages[0].data.role).toBe('subscriber');
|
||||
});
|
||||
|
||||
test("should simulate SFU signaling message exchange", () => {
|
||||
const publisherWs = new MockWebSocket("ws://localhost:3001?role=publisher");
|
||||
const subscriberWs = new MockWebSocket("ws://localhost:3001?role=subscriber");
|
||||
|
||||
const publisherMessages: any[] = [];
|
||||
const subscriberMessages: any[] = [];
|
||||
|
||||
publisherWs.onmessage = (event) => {
|
||||
publisherMessages.push(JSON.parse(event.data));
|
||||
};
|
||||
|
||||
subscriberWs.onmessage = (event) => {
|
||||
subscriberMessages.push(JSON.parse(event.data));
|
||||
};
|
||||
|
||||
// Simulate SFU RTC capabilities exchange
|
||||
publisherWs.simulateMessage({
|
||||
type: 'routerRtpCapabilities',
|
||||
data: {
|
||||
rtpCapabilities: {
|
||||
codecs: [
|
||||
{ mimeType: 'video/VP8', clockRate: 90000 },
|
||||
{ mimeType: 'audio/opus', clockRate: 48000 }
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Simulate transport creation
|
||||
publisherWs.simulateMessage({
|
||||
type: 'webRtcTransportCreated',
|
||||
data: {
|
||||
id: 'transport-123',
|
||||
iceParameters: { usernameFragment: 'test' },
|
||||
iceCandidates: [],
|
||||
dtlsParameters: { fingerprints: [] }
|
||||
}
|
||||
});
|
||||
|
||||
// Simulate producer created
|
||||
publisherWs.simulateMessage({
|
||||
type: 'produced',
|
||||
data: { producerId: 'producer-123' }
|
||||
});
|
||||
|
||||
// Notify subscriber of new producer
|
||||
subscriberWs.simulateMessage({
|
||||
type: 'newProducer',
|
||||
data: { producerId: 'producer-123' }
|
||||
});
|
||||
|
||||
// Simulate consumer created for subscriber
|
||||
subscriberWs.simulateMessage({
|
||||
type: 'consumed',
|
||||
data: {
|
||||
consumerId: 'consumer-123',
|
||||
producerId: 'producer-123',
|
||||
kind: 'video',
|
||||
rtpParameters: {}
|
||||
}
|
||||
});
|
||||
|
||||
// Verify message flow
|
||||
expect(publisherMessages).toHaveLength(3);
|
||||
expect(publisherMessages[0].type).toBe('routerRtpCapabilities');
|
||||
expect(publisherMessages[1].type).toBe('webRtcTransportCreated');
|
||||
expect(publisherMessages[2].type).toBe('produced');
|
||||
|
||||
expect(subscriberMessages).toHaveLength(2);
|
||||
expect(subscriberMessages[0].type).toBe('newProducer');
|
||||
expect(subscriberMessages[1].type).toBe('consumed');
|
||||
expect(subscriberMessages[1].data.producerId).toBe('producer-123');
|
||||
});
|
||||
|
||||
test("should simulate SFU scaling advantages", () => {
|
||||
// Simulate 1 publisher and multiple subscribers
|
||||
const publisher = new MockWebSocket("ws://localhost:3001?role=publisher");
|
||||
const subscribers = [
|
||||
new MockWebSocket("ws://localhost:3001?role=subscriber"),
|
||||
new MockWebSocket("ws://localhost:3001?role=subscriber"),
|
||||
new MockWebSocket("ws://localhost:3001?role=subscriber"),
|
||||
new MockWebSocket("ws://localhost:3001?role=subscriber"),
|
||||
new MockWebSocket("ws://localhost:3001?role=subscriber")
|
||||
];
|
||||
|
||||
// In mesh architecture, publisher would need 5 connections
|
||||
// In SFU architecture, publisher needs only 1 connection to server
|
||||
|
||||
const publisherConnections = 1; // SFU: constant regardless of subscribers
|
||||
const meshConnections = subscribers.length; // Mesh: scales with subscribers
|
||||
|
||||
const bandwidthSaved = (meshConnections - publisherConnections) / meshConnections;
|
||||
|
||||
expect(publisherConnections).toBe(1);
|
||||
expect(meshConnections).toBe(5);
|
||||
expect(bandwidthSaved).toBe(0.8); // 80% bandwidth saved for publisher
|
||||
});
|
||||
|
||||
test("should simulate SFU adaptive bitrate", () => {
|
||||
const subscribers = [
|
||||
{ quality: 'low', expectedBitrate: 800 },
|
||||
{ quality: 'medium', expectedBitrate: 1500 },
|
||||
{ quality: 'high', expectedBitrate: 2500 }
|
||||
];
|
||||
|
||||
// SFU can adapt bitrate per subscriber based on their capabilities
|
||||
subscribers.forEach(subscriber => {
|
||||
const ws = new MockWebSocket(`ws://localhost:3001?role=subscriber&quality=${subscriber.quality}`);
|
||||
const messages: any[] = [];
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
messages.push(JSON.parse(event.data));
|
||||
};
|
||||
|
||||
ws.simulateMessage({
|
||||
type: 'consumed',
|
||||
data: {
|
||||
consumerId: 'consumer-' + subscriber.quality,
|
||||
bitrate: subscriber.expectedBitrate,
|
||||
quality: subscriber.quality
|
||||
}
|
||||
});
|
||||
|
||||
expect(messages).toHaveLength(1);
|
||||
expect(messages[0].data.quality).toBe(subscriber.quality);
|
||||
expect(messages[0].data.bitrate).toBe(subscriber.expectedBitrate);
|
||||
});
|
||||
});
|
||||
|
||||
test("should simulate SFU error handling", () => {
|
||||
const client = new MockWebSocket("ws://localhost:3001?role=subscriber");
|
||||
const messages: any[] = [];
|
||||
|
||||
client.onmessage = (event) => {
|
||||
messages.push(JSON.parse(event.data));
|
||||
};
|
||||
|
||||
// Simulate server error
|
||||
client.simulateMessage({
|
||||
type: 'error',
|
||||
data: { message: 'Producer not found' }
|
||||
});
|
||||
|
||||
expect(messages).toHaveLength(1);
|
||||
expect(messages[0].type).toBe('error');
|
||||
expect(messages[0].data.message).toContain('Producer not found');
|
||||
});
|
||||
|
||||
test("should simulate SFU statistics tracking", () => {
|
||||
// Mock server statistics
|
||||
const stats = {
|
||||
totalClients: 6,
|
||||
publishers: 1,
|
||||
subscribers: 5,
|
||||
streamsForwarded: 1,
|
||||
totalBandwidth: 8500, // 1 publisher (2500) + 5 subscribers (1200 each)
|
||||
uptime: 120
|
||||
};
|
||||
|
||||
// Verify SFU efficiency metrics
|
||||
expect(stats.publishers).toBe(1);
|
||||
expect(stats.subscribers).toBe(5);
|
||||
expect(stats.streamsForwarded).toBe(1); // Server handles 1 stream, forwards to 5
|
||||
|
||||
// Calculate bandwidth efficiency
|
||||
const publisherBandwidth = 2500;
|
||||
const meshTotalBandwidth = publisherBandwidth * stats.subscribers; // 12,500
|
||||
const sfuTotalBandwidth = stats.totalBandwidth; // 8,500
|
||||
const bandwidthSaved = (meshTotalBandwidth - sfuTotalBandwidth) / meshTotalBandwidth;
|
||||
|
||||
expect(bandwidthSaved).toBeGreaterThan(0.3); // At least 30% bandwidth saved
|
||||
});
|
||||
|
||||
test("should simulate SFU reconnection handling", () => {
|
||||
const client = new MockWebSocket("ws://localhost:3001?role=publisher");
|
||||
let reconnectAttempted = false;
|
||||
|
||||
client.onclose = () => {
|
||||
reconnectAttempted = true;
|
||||
};
|
||||
|
||||
// Simulate connection loss
|
||||
client.close();
|
||||
|
||||
expect(reconnectAttempted).toBe(true);
|
||||
expect(client.readyState).toBe(WebSocket.CLOSED);
|
||||
|
||||
// In a real SFU implementation, the client would reconnect
|
||||
// and the server would maintain stream state
|
||||
const reconnectedClient = new MockWebSocket("ws://localhost:3001?role=publisher");
|
||||
expect(reconnectedClient.readyState).toBe(WebSocket.OPEN);
|
||||
});
|
||||
|
||||
test("should validate SFU message types", () => {
|
||||
const validSFUMessageTypes = [
|
||||
'join',
|
||||
'routerRtpCapabilities',
|
||||
'webRtcTransportCreated',
|
||||
'webRtcTransportConnected',
|
||||
'produced',
|
||||
'consumed',
|
||||
'resumed',
|
||||
'paused',
|
||||
'newProducer',
|
||||
'producers',
|
||||
'error'
|
||||
];
|
||||
|
||||
validSFUMessageTypes.forEach(messageType => {
|
||||
expect(typeof messageType).toBe('string');
|
||||
expect(messageType.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// Test message structure
|
||||
const sampleMessage = {
|
||||
type: 'consumed',
|
||||
data: {
|
||||
consumerId: 'consumer-123',
|
||||
producerId: 'producer-123',
|
||||
kind: 'video'
|
||||
}
|
||||
};
|
||||
|
||||
expect(sampleMessage.type).toBe('consumed');
|
||||
expect(sampleMessage.data.consumerId).toBe('consumer-123');
|
||||
expect(sampleMessage.data.producerId).toBe('producer-123');
|
||||
expect(sampleMessage.data.kind).toBe('video');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user