Files
beStream/src/services/streaming/streamingService.ts
2025-12-19 13:48:48 +01:00

213 lines
5.4 KiB
TypeScript

import axios from 'axios';
import { getApiUrl } from '../../utils/platform';
// Get API URL using platform-aware resolution
// Defaults to http://localhost:3001 for both Electron and Android
const getApiUrlValue = () => getApiUrl();
export interface StreamSession {
sessionId: string;
status: 'connecting' | 'downloading' | 'ready' | 'error';
progress: number;
downloadSpeed: number;
uploadSpeed: number;
peers: number;
downloaded: number;
total: number;
videoFile?: {
name: string;
size: number;
path?: string;
};
error?: string;
}
export interface TranscodeProgress {
type: 'transcode';
progress: number;
time?: string;
status?: 'ready';
playlistUrl?: string;
}
class StreamingService {
private ws: WebSocket | null = null;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private listeners: Map<string, Set<(data: unknown) => void>> = new Map();
/**
* Connect to WebSocket for real-time updates
*/
connect(sessionId: string): Promise<void> {
return new Promise((resolve, reject) => {
try {
const apiUrl = getApiUrlValue();
const wsUrl = apiUrl.replace('http://', 'ws://').replace('https://', 'wss://') + '/ws';
this.ws = new WebSocket(wsUrl);
this.ws.onopen = () => {
console.log('WebSocket connected');
this.reconnectAttempts = 0;
// Subscribe to session updates
this.ws?.send(JSON.stringify({ type: 'subscribe', sessionId }));
resolve();
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.emit(sessionId, data);
} catch (e) {
console.error('WebSocket message parse error:', e);
}
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
this.ws.onclose = () => {
console.log('WebSocket disconnected');
this.attemptReconnect(sessionId);
};
} catch (error) {
reject(error);
}
});
}
/**
* Attempt to reconnect WebSocket
*/
private attemptReconnect(sessionId: string) {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
console.log(`Reconnecting... attempt ${this.reconnectAttempts}`);
setTimeout(() => this.connect(sessionId), 2000 * this.reconnectAttempts);
}
}
/**
* Subscribe to session updates
*/
subscribe(sessionId: string, callback: (data: unknown) => void): () => void {
if (!this.listeners.has(sessionId)) {
this.listeners.set(sessionId, new Set());
}
this.listeners.get(sessionId)!.add(callback);
// Return unsubscribe function
return () => {
this.listeners.get(sessionId)?.delete(callback);
};
}
/**
* Emit event to listeners
*/
private emit(sessionId: string, data: unknown) {
this.listeners.get(sessionId)?.forEach((callback) => callback(data));
}
/**
* Disconnect WebSocket
*/
disconnect() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.listeners.clear();
}
/**
* Start a new streaming session
*/
async startStream(
hash: string,
name: string,
quality?: string
): Promise<{ sessionId: string; status: string; videoFile?: { name: string; size: number } }> {
const apiUrl = getApiUrlValue();
const response = await axios.post(`${apiUrl}/api/stream/start`, {
hash,
name,
quality,
});
return response.data;
}
/**
* Get session status
*/
async getStatus(sessionId: string): Promise<StreamSession> {
const apiUrl = getApiUrlValue();
const response = await axios.get(`${apiUrl}/api/stream/${sessionId}/status`);
return response.data;
}
/**
* Get direct video stream URL
*/
getVideoUrl(sessionId: string): string {
const apiUrl = getApiUrlValue();
return `${apiUrl}/api/stream/${sessionId}/video`;
}
/**
* Start HLS transcoding
*/
async startHls(sessionId: string): Promise<{ status: string; playlistUrl?: string }> {
const apiUrl = getApiUrlValue();
const response = await axios.post(`${apiUrl}/api/stream/${sessionId}/hls`);
return response.data;
}
/**
* Get HLS playlist URL
*/
getHlsUrl(sessionId: string): string {
const apiUrl = getApiUrlValue();
return `${apiUrl}/api/stream/${sessionId}/hls/playlist.m3u8`;
}
/**
* Get video info (duration, resolution, etc.)
*/
async getVideoInfo(sessionId: string): Promise<{
duration: number;
video?: { codec: string; width: number; height: number };
audio?: { codec: string; channels: number };
}> {
const apiUrl = getApiUrlValue();
const response = await axios.get(`${apiUrl}/api/stream/${sessionId}/info`);
return response.data;
}
/**
* Stop streaming session
*/
async stopStream(sessionId: string): Promise<void> {
const apiUrl = getApiUrlValue();
await axios.delete(`${apiUrl}/api/stream/${sessionId}`);
}
/**
* Check if server is healthy
*/
async checkHealth(): Promise<{ status: string; activeTorrents: number }> {
try {
const apiUrl = getApiUrlValue();
const response = await axios.get(`${apiUrl}/health`);
return response.data;
} catch {
throw new Error('Streaming server is not available');
}
}
}
export const streamingService = new StreamingService();
export default streamingService;