213 lines
5.4 KiB
TypeScript
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;
|
|
|