RobotHub-Frontend / src /lib /elements /video /videoStreaming.svelte.ts
blanchon's picture
Update
3165745
/**
* Video Streaming System - Input/Output Architecture
* Clean separation between input sources and output destinations
*/
import { video } from '@robothub/transport-server-client';
import type { video as videoTypes } from '@robothub/transport-server-client';
import { settings } from '$lib/runes/settings.svelte';
// Input/Output state using runes
export class VideoStreamingState {
// Input state (what you're viewing)
input = $state({
type: null as 'local-camera' | 'remote-stream' | null,
stream: null as MediaStream | null,
client: null as videoTypes.VideoConsumer | null,
roomId: null as string | null,
});
// Output state (what you're broadcasting)
output = $state({
active: false,
client: null as videoTypes.VideoProducer | null,
roomId: null as string | null,
});
// Room listing state
rooms = $state<videoTypes.RoomInfo[]>([]);
roomsLoading = $state(false);
// Derived state
get hasInput() {
return this.input.type !== null && this.input.stream !== null;
}
get hasOutput() {
return this.output.active;
}
get canOutput() {
// Can only output if input is local camera (not remote stream)
return this.input.type === 'local-camera' && this.input.stream !== null;
}
get currentStream() {
return this.input.stream;
}
}
// Create global instance
export const videoStreaming = new VideoStreamingState();
// External action functions
export const videoActions = {
// Room management
async listRooms(workspaceId: string): Promise<videoTypes.RoomInfo[]> {
videoStreaming.roomsLoading = true;
try {
const client = new video.VideoClientCore(settings.transportServerUrl);
const rooms = await client.listRooms(workspaceId);
videoStreaming.rooms = rooms;
return rooms;
} catch (error) {
console.error('Failed to list rooms:', error);
videoStreaming.rooms = [];
return [];
} finally {
videoStreaming.roomsLoading = false;
}
},
// Input actions
async connectLocalCamera(): Promise<{ success: boolean; error?: string }> {
try {
// Get local camera stream - no server connection needed for local viewing
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: 1280, height: 720 },
audio: true
});
// First disconnect any existing input to avoid conflicts
await this.disconnectInput();
// Update input state - purely local, no server interaction
videoStreaming.input.type = 'local-camera';
videoStreaming.input.stream = stream;
videoStreaming.input.client = null;
videoStreaming.input.roomId = null;
console.log('Local camera connected (local viewing only)');
return { success: true };
} catch (error) {
console.error('Failed to connect local camera:', error);
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
},
async connectRemoteStream(workspaceId: string, roomId: string): Promise<{ success: boolean; error?: string }> {
try {
// First disconnect any existing input
await this.disconnectInput();
const consumer = new video.VideoConsumer(settings.transportServerUrl);
const connected = await consumer.connect(workspaceId, roomId, 'consumer-id');
if (!connected) {
throw new Error('Failed to connect to remote stream');
}
// Start receiving video
await consumer.startReceiving();
// Set up stream receiving
consumer.on('streamReceived', (stream: MediaStream) => {
videoStreaming.input.stream = stream;
});
// Update input state
videoStreaming.input.type = 'remote-stream';
videoStreaming.input.client = consumer;
videoStreaming.input.roomId = roomId;
console.log('Remote stream connected');
return { success: true };
} catch (error) {
console.error('Failed to connect remote stream:', error);
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
},
async disconnectInput(): Promise<void> {
// Stop local camera tracks if any
if (videoStreaming.input.stream && videoStreaming.input.type === 'local-camera') {
videoStreaming.input.stream.getTracks().forEach(track => track.stop());
}
// Disconnect remote client if any
if (videoStreaming.input.client) {
videoStreaming.input.client.disconnect();
}
// Reset input state
videoStreaming.input.type = null;
videoStreaming.input.stream = null;
videoStreaming.input.client = null;
videoStreaming.input.roomId = null;
console.log('Input disconnected');
},
// Output actions
async startOutput(workspaceId: string): Promise<{ success: boolean; error?: string; roomId?: string }> {
if (!videoStreaming.canOutput) {
return { success: false, error: 'Cannot output - input must be local camera' };
}
try {
const producer = new video.VideoProducer(settings.transportServerUrl);
// Create room
const roomData = await producer.createRoom(workspaceId);
const connected = await producer.connect(roomData.workspaceId, roomData.roomId, 'producer-id');
if (!connected) {
throw new Error('Failed to connect producer');
}
// Use the current input stream for output by starting camera with existing stream
if (videoStreaming.input.stream) {
// We need to use the producer's startCamera method properly
// For now, we'll start a new camera stream since we can't directly use existing stream
await producer.startCamera({
video: { width: 1280, height: 720 },
audio: true
});
}
// Update output state
videoStreaming.output.active = true;
videoStreaming.output.client = producer;
videoStreaming.output.roomId = roomData.roomId;
// Refresh room list
await this.listRooms(workspaceId);
console.log('Output started, room created:', roomData.roomId);
return { success: true, roomId: roomData.roomId };
} catch (error) {
console.error('Failed to start output:', error);
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
},
async stopOutput(): Promise<void> {
if (videoStreaming.output.client) {
videoStreaming.output.client.disconnect();
}
// Reset output state
videoStreaming.output.active = false;
videoStreaming.output.client = null;
videoStreaming.output.roomId = null;
console.log('Output stopped');
},
// Utility functions
async refreshRooms(workspaceId: string): Promise<void> {
await this.listRooms(workspaceId);
},
getAvailableRooms(): videoTypes.RoomInfo[] {
return videoStreaming.rooms.filter(room => room.participants.producer !== null);
},
getRoomById(roomId: string): videoTypes.RoomInfo | undefined {
return videoStreaming.rooms.find(room => room.id === roomId);
}
};