feat: frontend session management

This commit is contained in:
Ante Brähler
2024-12-28 00:21:06 +01:00
parent c3bcfb4378
commit ac0422ef94
10 changed files with 170 additions and 17 deletions

View File

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

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -1,3 +1,4 @@
//Implement this interface for your specific session provider
class SessionHandler {
constructor() {
if (this.constructor === SessionHandler) {

View File

@@ -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);
}
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}`;
}

View File

@@ -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) {

View File

@@ -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
*/

View 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;

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

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