feat: frontend session management
This commit is contained in:
@@ -4,7 +4,7 @@ const ProxyHelperService = require('../services/proxy/ProxyHelperService');
|
||||
|
||||
module.exports = {
|
||||
channel(req, res) {
|
||||
let { url: targetUrl, channelId, headers } = req.query;
|
||||
let { url: targetUrl, channelId, headers, id } = req.query;
|
||||
|
||||
if(!targetUrl) {
|
||||
const channel = channelId ?
|
||||
@@ -16,6 +16,11 @@ module.exports = {
|
||||
return;
|
||||
}
|
||||
targetUrl = channel.url;
|
||||
|
||||
if(id) {
|
||||
targetUrl += `?id=${id}`;
|
||||
}
|
||||
|
||||
if(channel.headers && channel.headers.length > 0) {
|
||||
headers = Buffer.from(JSON.stringify(channel.headers)).toString('base64');
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ async function start(nextChannel) {
|
||||
|
||||
nextChannel.sessionProvider = SessionFactory.getSessionProvider(nextChannel.url);
|
||||
if(nextChannel.sessionProvider) {
|
||||
await nextChannel.sessionProvider.createSession(nextChannel.url);
|
||||
await nextChannel.sessionProvider.createSession();
|
||||
}
|
||||
|
||||
ffmpegService.startFFmpeg(nextChannel);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
const StreamedSuSession = require('./StreamedSuSession');
|
||||
|
||||
class SessionFactory {
|
||||
static getSessionProvider(channelDomain) {
|
||||
static getSessionProvider(channelUrl) {
|
||||
switch (true) {
|
||||
case channelDomain.includes('vipstreams.in'): //StreamedSU
|
||||
return new StreamedSuSession('https://secure.embedme.top');
|
||||
case channelUrl.includes('vipstreams.in'): //StreamedSU
|
||||
return new StreamedSuSession(channelUrl, 'https://secure.embedme.top');
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//Implement this interface for your specific session provider
|
||||
class SessionHandler {
|
||||
constructor() {
|
||||
if (this.constructor === SessionHandler) {
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
const SessionHandler = require('./SessionHandler');
|
||||
|
||||
class StreamedSuSession extends SessionHandler {
|
||||
constructor(baseUrl) {
|
||||
constructor(channelUrl, baseUrl) {
|
||||
super();
|
||||
this.channelUrl = channelUrl;
|
||||
this.baseUrl = baseUrl;
|
||||
this.checkInterval = null;
|
||||
this.sessionData = null;
|
||||
}
|
||||
|
||||
async #initSession(url) {
|
||||
async #initSession() {
|
||||
|
||||
console.log('Creating session:', url);
|
||||
console.log('Creating session:', this.channelUrl);
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/init-session`, {
|
||||
method: "POST",
|
||||
@@ -18,7 +19,7 @@ class StreamedSuSession extends SessionHandler {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
path: new URL(url).pathname,
|
||||
path: new URL(this.channelUrl).pathname,
|
||||
})
|
||||
});
|
||||
|
||||
@@ -58,7 +59,7 @@ class StreamedSuSession extends SessionHandler {
|
||||
const isValid = await this.#checkSession();
|
||||
if (!isValid) {
|
||||
console.log('Session aborted');
|
||||
this.destroySession();
|
||||
this.#initSession();
|
||||
}
|
||||
}, interval);
|
||||
}
|
||||
@@ -74,11 +75,11 @@ class StreamedSuSession extends SessionHandler {
|
||||
|
||||
// Public Methods
|
||||
|
||||
async createSession(url, interval = 15000) {
|
||||
async createSession(interval = 15000) {
|
||||
if (!this.sessionData) {
|
||||
await this.#initSession(url);
|
||||
}
|
||||
await this.#initSession();
|
||||
this.#startAutoCheck(interval);
|
||||
}
|
||||
return this.getSessionQuery();
|
||||
}
|
||||
|
||||
@@ -90,7 +91,7 @@ class StreamedSuSession extends SessionHandler {
|
||||
|
||||
getSessionQuery() {
|
||||
if (!this.sessionData?.id) {
|
||||
throw new Error('No active session');
|
||||
return '';
|
||||
}
|
||||
return `id=${this.sessionData.id}`;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useContext, useEffect, useRef } from 'react';
|
||||
import Hls from 'hls.js';
|
||||
import { Channel, ChannelMode } from '../types';
|
||||
import { ToastContext } from './notifications/ToastContext';
|
||||
import SessionFactory from '../services/session/SessionFactory';
|
||||
|
||||
interface VideoPlayerProps {
|
||||
channel: Channel | null;
|
||||
@@ -12,8 +13,10 @@ function VideoPlayer({ channel, syncEnabled }: VideoPlayerProps) {
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const hlsRef = useRef<Hls | null>(null);
|
||||
const { addToast, removeToast, clearToasts, editToast } = useContext(ToastContext);
|
||||
const sessionProvider = channel ? SessionFactory.getSessionProvider(channel.url) : null;
|
||||
|
||||
useEffect(() => {
|
||||
const setupVideoPlayer = async () => {
|
||||
if (!videoRef.current || !channel?.url) return;
|
||||
const video = videoRef.current;
|
||||
|
||||
@@ -50,9 +53,19 @@ function VideoPlayer({ channel, syncEnabled }: VideoPlayerProps) {
|
||||
},
|
||||
});
|
||||
|
||||
let sessionQuery = null;
|
||||
if(channel.mode !== 'restream' && sessionProvider && SessionFactory.checkSessionProvider(channel.url)) {
|
||||
await sessionProvider.createSession();
|
||||
sessionQuery = sessionProvider.getSessionQuery();
|
||||
} else {
|
||||
sessionProvider?.destroySession();
|
||||
}
|
||||
|
||||
const querySeparator = channel.url.includes('?') ? '&' : '?';
|
||||
const sourceLinks: Record<ChannelMode, string> = {
|
||||
direct: channel.url,
|
||||
proxy: import.meta.env.VITE_BACKEND_URL + '/proxy/channel', //TODO: needs update for multi-channel streaming
|
||||
direct: sessionQuery ? channel.url + querySeparator + sessionQuery : channel.url,
|
||||
//TODO: needs update for multi-channel streaming
|
||||
proxy: sessionQuery ? import.meta.env.VITE_BACKEND_URL + '/proxy/channel?' + sessionQuery : import.meta.env.VITE_BACKEND_URL + '/proxy/channel',
|
||||
restream: import.meta.env.VITE_BACKEND_URL + '/streams/' + channel.id + "/" + channel.id + ".m3u8", //e.g. http://backend:3000/streams/1/1.m3u8
|
||||
};
|
||||
|
||||
@@ -214,7 +227,9 @@ function VideoPlayer({ channel, syncEnabled }: VideoPlayerProps) {
|
||||
hlsRef.current.destroy();
|
||||
}
|
||||
};
|
||||
}, [channel?.url, channel?.mode, syncEnabled]);
|
||||
}
|
||||
setupVideoPlayer();
|
||||
}, [channel?.url, channel?.mode, syncEnabled, sessionProvider?.getSessionQuery()]);
|
||||
|
||||
const handleVideoClick = (event: React.MouseEvent<HTMLVideoElement>) => {
|
||||
if (videoRef.current?.muted) {
|
||||
|
||||
@@ -7,6 +7,7 @@ const apiService = {
|
||||
* Execute API request
|
||||
* @param path - Path (e.g. "/channels/")
|
||||
* @param method - HTTP-Method (GET, POST, etc.)
|
||||
* @param api_url - The API URL (default: API_BASE_URL + '/api')
|
||||
* @param body - The request body (e.g. POST)
|
||||
* @returns Ein Promise with the parsed JSON response to class T
|
||||
*/
|
||||
|
||||
19
frontend/src/services/session/SessionFactory.ts
Normal file
19
frontend/src/services/session/SessionFactory.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { SessionHandler } from "./SessionHandler";
|
||||
import { StreamedSuSession } from "./StreamedSuSession";
|
||||
|
||||
class SessionFactory {
|
||||
static getSessionProvider(channelUrl: string): SessionHandler | null {
|
||||
switch (true) {
|
||||
case channelUrl.includes('vipstreams.in'): //StreamedSU
|
||||
return new StreamedSuSession(channelUrl, 'https://secure.embedme.top');
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static checkSessionProvider(channelUrl: string): boolean {
|
||||
return !!SessionFactory.getSessionProvider(channelUrl);
|
||||
}
|
||||
}
|
||||
|
||||
export default SessionFactory;
|
||||
8
frontend/src/services/session/SessionHandler.ts
Normal file
8
frontend/src/services/session/SessionHandler.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
//Implement this interface for your specific session provider
|
||||
interface SessionHandler {
|
||||
createSession(interval?: number): Promise<string>;
|
||||
destroySession(): boolean;
|
||||
getSessionQuery(): string;
|
||||
}
|
||||
|
||||
export type { SessionHandler };
|
||||
103
frontend/src/services/session/StreamedSuSession.ts
Normal file
103
frontend/src/services/session/StreamedSuSession.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { SessionHandler } from "./SessionHandler";
|
||||
|
||||
class StreamedSuSession implements SessionHandler {
|
||||
private baseUrl: string;
|
||||
private channelUrl: string;
|
||||
private checkInterval: number | null;
|
||||
private sessionId: string | null;
|
||||
|
||||
constructor(channelUrl: string, baseUrl: string) {
|
||||
this.channelUrl = channelUrl;
|
||||
this.baseUrl = baseUrl;
|
||||
this.checkInterval = null;
|
||||
this.sessionId = null;
|
||||
}
|
||||
|
||||
private async initSession(): Promise<any> {
|
||||
console.log('Creating session:', this.channelUrl);
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/init-session`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
path: new URL(this.channelUrl).pathname,
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to initialize session');
|
||||
}
|
||||
|
||||
const sessionData = await response.json();
|
||||
this.sessionId = sessionData.id;
|
||||
return sessionData.id;
|
||||
} catch (error) {
|
||||
console.error('Session initialization failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async checkSession(): Promise<boolean> {
|
||||
if (!this.sessionId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('Checking session:', this.sessionId);
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/check/${this.sessionId}`);
|
||||
return response.status === 200;
|
||||
} catch (error) {
|
||||
console.error('Session check failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private startAutoCheck(interval: number = 15000): void {
|
||||
if (this.checkInterval) {
|
||||
this.stopAutoCheck();
|
||||
}
|
||||
|
||||
this.checkInterval = window.setInterval(async () => {
|
||||
const isValid = await this.checkSession();
|
||||
if (!isValid) {
|
||||
console.log('Session aborted');
|
||||
this.initSession();
|
||||
}
|
||||
}, interval);
|
||||
}
|
||||
|
||||
private stopAutoCheck(): void {
|
||||
if (this.checkInterval) {
|
||||
window.clearInterval(this.checkInterval);
|
||||
this.checkInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Public Methods
|
||||
async createSession(interval: number = 15000): Promise<string> {
|
||||
if (!this.sessionId) {
|
||||
await this.initSession();
|
||||
this.startAutoCheck(interval);
|
||||
}
|
||||
return this.getSessionQuery();
|
||||
}
|
||||
|
||||
destroySession(): boolean {
|
||||
console.log('Destroying session:', this.sessionId);
|
||||
this.stopAutoCheck();
|
||||
this.sessionId = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
getSessionQuery(): string {
|
||||
console.log('Session ID:', this.sessionId);
|
||||
if (!this.sessionId) {
|
||||
return '';
|
||||
}
|
||||
return `id=${this.sessionId}`;
|
||||
}
|
||||
}
|
||||
|
||||
export { StreamedSuSession };
|
||||
Reference in New Issue
Block a user