fixed tests

This commit is contained in:
2025-09-05 00:36:54 -04:00
commit a536668a0b
48 changed files with 8187 additions and 0 deletions

View File

@@ -0,0 +1,344 @@
import { test, expect, describe, beforeEach } from "bun:test";
import { SFUClientManager } from "../../src/services/SFUClientManager.ts";
import type { ISFUClient } from "../../src/interfaces/ISFUTypes.ts";
// Mock WebRTC Transport
class MockWebRtcTransport {
id: string;
closed = false;
constructor(id: string) {
this.id = id;
}
close() {
this.closed = true;
}
}
// Mock Producer
class MockProducer {
id: string;
closed = false;
constructor(id: string) {
this.id = id;
}
close() {
this.closed = true;
}
}
// Mock Consumer
class MockConsumer {
id: string;
closed = false;
constructor(id: string) {
this.id = id;
}
close() {
this.closed = true;
}
}
// Mock WebSocket
class MockWebSocket {
sentMessages: string[] = [];
send(data: string) {
this.sentMessages.push(data);
}
}
describe("SFUClientManager", () => {
let clientManager: SFUClientManager;
beforeEach(() => {
clientManager = new SFUClientManager();
});
test("should add and retrieve clients", () => {
const mockWs = new MockWebSocket();
const client: ISFUClient = {
id: "client-1",
role: "publisher",
ws: mockWs,
consumers: new Map()
};
clientManager.addClient(client);
const retrievedClient = clientManager.getClient("client-1");
expect(retrievedClient).toBe(client);
});
test("should set and manage client transport", () => {
const mockWs = new MockWebSocket();
const client: ISFUClient = {
id: "client-1",
role: "publisher",
ws: mockWs,
consumers: new Map()
};
clientManager.addClient(client);
const transport = new MockWebRtcTransport("transport-1") as any;
clientManager.setClientTransport("client-1", transport);
const retrievedClient = clientManager.getClient("client-1");
expect(retrievedClient?.transport).toBe(transport);
});
test("should manage client producer", () => {
const mockWs = new MockWebSocket();
const client: ISFUClient = {
id: "publisher-1",
role: "publisher",
ws: mockWs,
consumers: new Map()
};
clientManager.addClient(client);
const producer = new MockProducer("producer-1") as any;
clientManager.setClientProducer("publisher-1", producer);
const retrievedClient = clientManager.getClient("publisher-1");
expect(retrievedClient?.producer).toBe(producer);
const retrievedProducer = clientManager.getProducer("producer-1");
expect(retrievedProducer).toBe(producer);
});
test("should manage client consumers", () => {
const mockWs = new MockWebSocket();
const client: ISFUClient = {
id: "subscriber-1",
role: "subscriber",
ws: mockWs,
consumers: new Map()
};
clientManager.addClient(client);
const consumer = new MockConsumer("consumer-1") as any;
clientManager.addClientConsumer("subscriber-1", consumer);
const retrievedClient = clientManager.getClient("subscriber-1");
expect(retrievedClient?.consumers.has("consumer-1")).toBe(true);
expect(retrievedClient?.consumers.get("consumer-1")).toBe(consumer);
const retrievedConsumer = clientManager.getConsumer("consumer-1");
expect(retrievedConsumer).toBe(consumer);
});
test("should remove client consumer", () => {
const mockWs = new MockWebSocket();
const client: ISFUClient = {
id: "subscriber-1",
role: "subscriber",
ws: mockWs,
consumers: new Map()
};
clientManager.addClient(client);
const consumer = new MockConsumer("consumer-1") as any;
clientManager.addClientConsumer("subscriber-1", consumer);
// Verify consumer is added
expect(clientManager.getConsumer("consumer-1")).toBe(consumer);
// Remove consumer
clientManager.removeClientConsumer("subscriber-1", "consumer-1");
// Verify consumer is removed and closed
expect(clientManager.getConsumer("consumer-1")).toBeUndefined();
expect(consumer.closed).toBe(true);
const retrievedClient = clientManager.getClient("subscriber-1");
expect(retrievedClient?.consumers.has("consumer-1")).toBe(false);
});
test("should get publishers and subscribers separately", () => {
const publisherWs = new MockWebSocket();
const subscriberWs1 = new MockWebSocket();
const subscriberWs2 = new MockWebSocket();
const publisher: ISFUClient = {
id: "publisher-1",
role: "publisher",
ws: publisherWs,
consumers: new Map(),
producer: new MockProducer("producer-1") as any
};
const subscriber1: ISFUClient = {
id: "subscriber-1",
role: "subscriber",
ws: subscriberWs1,
consumers: new Map()
};
const subscriber2: ISFUClient = {
id: "subscriber-2",
role: "subscriber",
ws: subscriberWs2,
consumers: new Map()
};
clientManager.addClient(publisher);
clientManager.addClient(subscriber1);
clientManager.addClient(subscriber2);
const publishers = clientManager.getPublishers();
const subscribers = clientManager.getSubscribers();
expect(publishers).toHaveLength(1);
expect(publishers[0]).toBe(publisher);
expect(subscribers).toHaveLength(2);
expect(subscribers).toContain(subscriber1);
expect(subscribers).toContain(subscriber2);
});
test("should get all producers", () => {
const publisherWs1 = new MockWebSocket();
const publisherWs2 = new MockWebSocket();
const publisher1: ISFUClient = {
id: "publisher-1",
role: "publisher",
ws: publisherWs1,
consumers: new Map()
};
const publisher2: ISFUClient = {
id: "publisher-2",
role: "publisher",
ws: publisherWs2,
consumers: new Map()
};
clientManager.addClient(publisher1);
clientManager.addClient(publisher2);
const producer1 = new MockProducer("producer-1") as any;
const producer2 = new MockProducer("producer-2") as any;
clientManager.setClientProducer("publisher-1", producer1);
clientManager.setClientProducer("publisher-2", producer2);
const allProducers = clientManager.getAllProducers();
expect(allProducers).toHaveLength(2);
expect(allProducers).toContain(producer1);
expect(allProducers).toContain(producer2);
});
test("should remove client and cleanup resources", () => {
const mockWs = new MockWebSocket();
const client: ISFUClient = {
id: "client-1",
role: "publisher",
ws: mockWs,
consumers: new Map()
};
clientManager.addClient(client);
// Add transport, producer, and consumer
const transport = new MockWebRtcTransport("transport-1") as any;
const producer = new MockProducer("producer-1") as any;
const consumer = new MockConsumer("consumer-1") as any;
clientManager.setClientTransport("client-1", transport);
clientManager.setClientProducer("client-1", producer);
clientManager.addClientConsumer("client-1", consumer);
// Remove client
clientManager.removeClient("client-1");
// Verify client is removed
expect(clientManager.getClient("client-1")).toBeUndefined();
// Verify resources are cleaned up
expect(transport.closed).toBe(true);
expect(producer.closed).toBe(true);
expect(consumer.closed).toBe(true);
// Verify global maps are cleaned up
expect(clientManager.getProducer("producer-1")).toBeUndefined();
expect(clientManager.getConsumer("consumer-1")).toBeUndefined();
});
test("should provide correct statistics", () => {
const publisherWs = new MockWebSocket();
const subscriberWs1 = new MockWebSocket();
const subscriberWs2 = new MockWebSocket();
const publisher: ISFUClient = {
id: "publisher-1",
role: "publisher",
ws: publisherWs,
consumers: new Map(),
producer: new MockProducer("producer-1") as any
};
const subscriber1: ISFUClient = {
id: "subscriber-1",
role: "subscriber",
ws: subscriberWs1,
consumers: new Map()
};
const subscriber2: ISFUClient = {
id: "subscriber-2",
role: "subscriber",
ws: subscriberWs2,
consumers: new Map()
};
clientManager.addClient(publisher);
clientManager.addClient(subscriber1);
clientManager.addClient(subscriber2);
const producer = new MockProducer("producer-1") as any;
const consumer1 = new MockConsumer("consumer-1") as any;
const consumer2 = new MockConsumer("consumer-2") as any;
clientManager.setClientProducer("publisher-1", producer);
clientManager.addClientConsumer("subscriber-1", consumer1);
clientManager.addClientConsumer("subscriber-2", consumer2);
const stats = clientManager.getStats();
expect(stats.clients).toBe(3);
expect(stats.publishers).toBe(1);
expect(stats.subscribers).toBe(2);
expect(stats.producers).toBe(1);
expect(stats.consumers).toBe(2);
});
test("should handle removing non-existent client gracefully", () => {
expect(() => {
clientManager.removeClient("non-existent");
}).not.toThrow();
});
test("should handle operations on non-existent clients gracefully", () => {
const transport = new MockWebRtcTransport("transport-1") as any;
const producer = new MockProducer("producer-1") as any;
const consumer = new MockConsumer("consumer-1") as any;
expect(() => {
clientManager.setClientTransport("non-existent", transport);
clientManager.setClientProducer("non-existent", producer);
clientManager.addClientConsumer("non-existent", consumer);
clientManager.removeClientConsumer("non-existent", "consumer-1");
}).not.toThrow();
});
});

View 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');
});
});

View File

@@ -0,0 +1,441 @@
import { test, expect, describe, beforeEach } from "bun:test";
import { SFUSignalingService } from "../../src/services/SFUSignalingService.ts";
import { SFUClientManager } from "../../src/services/SFUClientManager.ts";
import type { ISFUSignalingMessage } from "../../src/interfaces/ISFUTypes.ts";
// Mock MediaServerManager
class MockMediaServerManager {
private mockRtpCapabilities = {
codecs: [
{ mimeType: 'video/VP8', clockRate: 90000 },
{ mimeType: 'audio/opus', clockRate: 48000 }
]
};
private mockTransport = {
id: 'transport-123',
iceParameters: { usernameFragment: 'test', password: 'test' },
iceCandidates: [],
dtlsParameters: { fingerprints: [{ algorithm: 'sha-256', value: 'test' }] },
connect: () => Promise.resolve(),
produce: () => Promise.resolve({ id: 'producer-123', kind: 'video' }),
consume: () => Promise.resolve({ id: 'consumer-123', kind: 'video', rtpParameters: {} }),
close: () => {}
};
private mockRouter = {
canConsume: () => true
};
getRtpCapabilities() {
return this.mockRtpCapabilities;
}
async createWebRtcTransport() {
return this.mockTransport;
}
getRouter() {
return this.mockRouter;
}
getWorker() {
return { pid: 12345 };
}
}
// Mock WebSocket
class MockWebSocket {
sentMessages: string[] = [];
send(data: string) {
this.sentMessages.push(data);
}
getLastMessage() {
return this.sentMessages.length > 0 ?
JSON.parse(this.sentMessages[this.sentMessages.length - 1]) : null;
}
getAllMessages() {
return this.sentMessages.map(msg => JSON.parse(msg));
}
}
describe("SFUSignalingService", () => {
let signalingService: SFUSignalingService;
let clientManager: SFUClientManager;
let mediaServer: MockMediaServerManager;
beforeEach(() => {
clientManager = new SFUClientManager();
mediaServer = new MockMediaServerManager();
signalingService = new SFUSignalingService(mediaServer as any, clientManager);
});
test("should handle client connections", () => {
const mockWs = new MockWebSocket();
const clientId = signalingService.handleConnection(mockWs, "publisher");
expect(typeof clientId).toBe("string");
expect(clientId.length).toBeGreaterThan(0);
const client = clientManager.getClient(clientId);
expect(client).toBeTruthy();
expect(client?.role).toBe("publisher");
expect(client?.ws).toBe(mockWs);
const lastMessage = mockWs.getLastMessage();
expect(lastMessage.type).toBe("join");
expect(lastMessage.data.clientId).toBe(clientId);
expect(lastMessage.data.role).toBe("publisher");
});
test("should handle client disconnection", () => {
const mockWs = new MockWebSocket();
const clientId = signalingService.handleConnection(mockWs, "subscriber");
expect(clientManager.getClient(clientId)).toBeTruthy();
signalingService.handleDisconnection(clientId);
expect(clientManager.getClient(clientId)).toBeUndefined();
});
test("should handle getRouterRtpCapabilities message", async () => {
const mockWs = new MockWebSocket();
const clientId = signalingService.handleConnection(mockWs, "publisher");
const message: ISFUSignalingMessage = {
type: "getRouterRtpCapabilities"
};
await signalingService.handleMessage(clientId, message);
const lastMessage = mockWs.getLastMessage();
expect(lastMessage.type).toBe("routerRtpCapabilities");
expect(lastMessage.data.rtpCapabilities).toBeTruthy();
expect(lastMessage.data.rtpCapabilities.codecs).toHaveLength(2);
});
test("should handle createWebRtcTransport message", async () => {
const mockWs = new MockWebSocket();
const clientId = signalingService.handleConnection(mockWs, "publisher");
const message: ISFUSignalingMessage = {
type: "createWebRtcTransport"
};
await signalingService.handleMessage(clientId, message);
const lastMessage = mockWs.getLastMessage();
expect(lastMessage.type).toBe("webRtcTransportCreated");
expect(lastMessage.data.id).toBe("transport-123");
expect(lastMessage.data.iceParameters).toBeTruthy();
expect(lastMessage.data.dtlsParameters).toBeTruthy();
// Verify transport was set on client
const client = clientManager.getClient(clientId);
expect(client?.transport).toBeTruthy();
});
test("should handle connectWebRtcTransport message", async () => {
const mockWs = new MockWebSocket();
const clientId = signalingService.handleConnection(mockWs, "publisher");
// First create transport
await signalingService.handleMessage(clientId, { type: "createWebRtcTransport" });
// Then connect it
const connectMessage: ISFUSignalingMessage = {
type: "connectWebRtcTransport",
data: {
dtlsParameters: { fingerprints: [{ algorithm: 'sha-256', value: 'test' }] }
}
};
await signalingService.handleMessage(clientId, connectMessage);
const lastMessage = mockWs.getLastMessage();
expect(lastMessage.type).toBe("webRtcTransportConnected");
});
test("should handle produce message for publisher", async () => {
const mockWs = new MockWebSocket();
const clientId = signalingService.handleConnection(mockWs, "publisher");
// Create transport first
await signalingService.handleMessage(clientId, { type: "createWebRtcTransport" });
// Mock producer creation
const mockProducer = { id: 'producer-123', kind: 'video' };
const client = clientManager.getClient(clientId);
if (client?.transport) {
(client.transport as any).produce = () => Promise.resolve(mockProducer);
}
const produceMessage: ISFUSignalingMessage = {
type: "produce",
data: {
kind: "video",
rtpParameters: { codecs: [], encodings: [] }
}
};
await signalingService.handleMessage(clientId, produceMessage);
const messages = mockWs.getAllMessages();
const producedMessage = messages.find(msg => msg.type === 'produced');
expect(producedMessage).toBeTruthy();
expect(producedMessage.data.producerId).toBe('producer-123');
});
test("should reject produce message for subscriber", async () => {
const mockWs = new MockWebSocket();
const clientId = signalingService.handleConnection(mockWs, "subscriber");
await signalingService.handleMessage(clientId, { type: "createWebRtcTransport" });
const produceMessage: ISFUSignalingMessage = {
type: "produce",
data: {
kind: "video",
rtpParameters: { codecs: [], encodings: [] }
}
};
await signalingService.handleMessage(clientId, produceMessage);
const lastMessage = mockWs.getLastMessage();
expect(lastMessage.type).toBe("error");
expect(lastMessage.data.message).toContain("Only publishers can produce");
});
test("should handle consume message for subscriber", async () => {
// First create a publisher with a producer
const publisherWs = new MockWebSocket();
const publisherId = signalingService.handleConnection(publisherWs, "publisher");
await signalingService.handleMessage(publisherId, { type: "createWebRtcTransport" });
// Mock producer
const mockProducer = { id: 'producer-123', kind: 'video' };
clientManager.setClientProducer(publisherId, mockProducer as any);
// Now create subscriber
const subscriberWs = new MockWebSocket();
const subscriberId = signalingService.handleConnection(subscriberWs, "subscriber");
await signalingService.handleMessage(subscriberId, { type: "createWebRtcTransport" });
// Mock consumer creation
const mockConsumer = {
id: 'consumer-123',
kind: 'video',
rtpParameters: { codecs: [], encodings: [] }
};
const subscriberClient = clientManager.getClient(subscriberId);
if (subscriberClient?.transport) {
(subscriberClient.transport as any).consume = () => Promise.resolve(mockConsumer);
}
const consumeMessage: ISFUSignalingMessage = {
type: "consume",
data: {
producerId: "producer-123",
rtpCapabilities: { codecs: [] }
}
};
await signalingService.handleMessage(subscriberId, consumeMessage);
const lastMessage = subscriberWs.getLastMessage();
expect(lastMessage.type).toBe("consumed");
expect(lastMessage.data.consumerId).toBe('consumer-123');
expect(lastMessage.data.producerId).toBe('producer-123');
});
test("should reject consume message for publisher", async () => {
const mockWs = new MockWebSocket();
const clientId = signalingService.handleConnection(mockWs, "publisher");
await signalingService.handleMessage(clientId, { type: "createWebRtcTransport" });
const consumeMessage: ISFUSignalingMessage = {
type: "consume",
data: {
producerId: "producer-123",
rtpCapabilities: { codecs: [] }
}
};
await signalingService.handleMessage(clientId, consumeMessage);
const lastMessage = mockWs.getLastMessage();
expect(lastMessage.type).toBe("error");
expect(lastMessage.data.message).toContain("Only subscribers can consume");
});
test("should handle resume message", async () => {
const mockWs = new MockWebSocket();
const clientId = signalingService.handleConnection(mockWs, "subscriber");
// Mock consumer
const mockConsumer = {
id: 'consumer-123',
resume: () => Promise.resolve()
};
clientManager.addClientConsumer(clientId, mockConsumer as any);
const resumeMessage: ISFUSignalingMessage = {
type: "resume",
data: { consumerId: "consumer-123" }
};
await signalingService.handleMessage(clientId, resumeMessage);
const lastMessage = mockWs.getLastMessage();
expect(lastMessage.type).toBe("resumed");
expect(lastMessage.data.consumerId).toBe("consumer-123");
});
test("should handle pause message", async () => {
const mockWs = new MockWebSocket();
const clientId = signalingService.handleConnection(mockWs, "subscriber");
// Mock consumer
const mockConsumer = {
id: 'consumer-123',
pause: () => Promise.resolve()
};
clientManager.addClientConsumer(clientId, mockConsumer as any);
const pauseMessage: ISFUSignalingMessage = {
type: "pause",
data: { consumerId: "consumer-123" }
};
await signalingService.handleMessage(clientId, pauseMessage);
const lastMessage = mockWs.getLastMessage();
expect(lastMessage.type).toBe("paused");
expect(lastMessage.data.consumerId).toBe("consumer-123");
});
test("should handle getProducers message", async () => {
// Create publisher with producer
const publisherWs = new MockWebSocket();
const publisherId = signalingService.handleConnection(publisherWs, "publisher");
const mockProducer = { id: 'producer-123', kind: 'video' };
clientManager.setClientProducer(publisherId, mockProducer as any);
// Create subscriber requesting producers
const subscriberWs = new MockWebSocket();
const subscriberId = signalingService.handleConnection(subscriberWs, "subscriber");
const getProducersMessage: ISFUSignalingMessage = {
type: "getProducers"
};
await signalingService.handleMessage(subscriberId, getProducersMessage);
const lastMessage = subscriberWs.getLastMessage();
expect(lastMessage.type).toBe("producers");
expect(lastMessage.data.producers).toHaveLength(1);
expect(lastMessage.data.producers[0].id).toBe('producer-123');
expect(lastMessage.data.producers[0].kind).toBe('video');
});
test("should notify subscribers when new producer joins", async () => {
// Create subscribers first
const subscriber1Ws = new MockWebSocket();
const subscriber1Id = signalingService.handleConnection(subscriber1Ws, "subscriber");
const subscriber2Ws = new MockWebSocket();
const subscriber2Id = signalingService.handleConnection(subscriber2Ws, "subscriber");
// Clear initial join messages
subscriber1Ws.sentMessages = [];
subscriber2Ws.sentMessages = [];
// Create publisher and produce
const publisherWs = new MockWebSocket();
const publisherId = signalingService.handleConnection(publisherWs, "publisher");
await signalingService.handleMessage(publisherId, { type: "createWebRtcTransport" });
// Mock producer creation and produce
const mockProducer = { id: 'producer-123', kind: 'video' };
const publisherClient = clientManager.getClient(publisherId);
if (publisherClient?.transport) {
(publisherClient.transport as any).produce = () => Promise.resolve(mockProducer);
}
await signalingService.handleMessage(publisherId, {
type: "produce",
data: { kind: "video", rtpParameters: { codecs: [], encodings: [] } }
});
// Check that subscribers were notified
const sub1LastMessage = subscriber1Ws.getLastMessage();
const sub2LastMessage = subscriber2Ws.getLastMessage();
expect(sub1LastMessage?.type).toBe("newProducer");
expect(sub1LastMessage?.data.producerId).toBe('producer-123');
expect(sub2LastMessage?.type).toBe("newProducer");
expect(sub2LastMessage?.data.producerId).toBe('producer-123');
});
test("should handle messages from unknown clients gracefully", async () => {
const message: ISFUSignalingMessage = {
type: "getRouterRtpCapabilities"
};
expect(async () => {
await signalingService.handleMessage("unknown-client-id", message);
}).not.toThrow();
});
test("should handle unknown message types", async () => {
const mockWs = new MockWebSocket();
const clientId = signalingService.handleConnection(mockWs, "publisher");
const unknownMessage: any = {
type: "unknownMessageType"
};
await signalingService.handleMessage(clientId, unknownMessage);
// Should not crash, just log a warning
expect(true).toBe(true); // Test passes if no exception thrown
});
test("should provide service statistics", () => {
// Create some clients
const publisher1Ws = new MockWebSocket();
const publisher1Id = signalingService.handleConnection(publisher1Ws, "publisher");
const subscriber1Ws = new MockWebSocket();
const subscriber1Id = signalingService.handleConnection(subscriber1Ws, "subscriber");
const subscriber2Ws = new MockWebSocket();
const subscriber2Id = signalingService.handleConnection(subscriber2Ws, "subscriber");
// Add some producers and consumers
const mockProducer = { id: 'producer-123', kind: 'video' };
const mockConsumer1 = { id: 'consumer-123', kind: 'video' };
const mockConsumer2 = { id: 'consumer-456', kind: 'video' };
clientManager.setClientProducer(publisher1Id, mockProducer as any);
clientManager.addClientConsumer(subscriber1Id, mockConsumer1 as any);
clientManager.addClientConsumer(subscriber2Id, mockConsumer2 as any);
const stats = signalingService.getStats();
expect(stats.clients).toBe(3);
expect(stats.publishers).toBe(1);
expect(stats.subscribers).toBe(2);
expect(stats.producers).toBe(1);
expect(stats.consumers).toBe(2);
expect(stats.mediaServer.workerId).toBe(12345);
});
});