From 1ff51fad619dcfc46131b0e426fd7d191f0fc5a5 Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Wed, 17 Dec 2025 18:33:21 +0100 Subject: [PATCH] feat: streaminfo and thumbnail wiring --- .../(protected)/api/mediamtx/publish/route.ts | 33 ++++----- .../app/StreamPlayer/StreamPlayer.tsx | 67 +++++++------------ .../web/src/lib/instrumentation/streamInfo.ts | 23 +++---- apps/web/src/lib/types/mediamtx.d.ts | 2 - apps/web/src/lib/workers/worker/thumbnails.ts | 32 ++++----- dev/docker-compose.yml | 1 + dev/mediamtx.yml | 4 +- 7 files changed, 64 insertions(+), 98 deletions(-) diff --git a/apps/web/src/app/(ui)/(protected)/api/mediamtx/publish/route.ts b/apps/web/src/app/(ui)/(protected)/api/mediamtx/publish/route.ts index a073e4d..b1c65de 100644 --- a/apps/web/src/app/(ui)/(protected)/api/mediamtx/publish/route.ts +++ b/apps/web/src/app/(ui)/(protected)/api/mediamtx/publish/route.ts @@ -1,8 +1,10 @@ import { prisma, getRedisConnection } from '@hctv/db'; import { NextRequest } from 'next/server'; import { z } from 'zod'; +import { lucia } from '@hctv/auth'; export async function POST(request: NextRequest) { + const redis = getRedisConnection(); const body = await request.json(); const parsed = schema.safeParse(body); @@ -12,35 +14,26 @@ export async function POST(request: NextRequest) { } const { action, protocol, path, password } = parsed.data; if (action === 'publish' && protocol === 'srt') { - const redis = getRedisConnection(); - const channelKey = await redis.get(`streamKey:${path}`) + const channelKey = await redis.get(`streamKey:${path}`); if (channelKey) { if (channelKey !== password) { return new Response('invalid stream key', { status: 403 }); } return new Response('youre in yay', { status: 200 }); - } else { - const key = await prisma.streamKey.findFirst({ - where: { - key: password, - channel: { - name: path, - } - }, - include: { - channel: true, - }, - }); - - if (!key) { - return new Response('invalid stream key', { status: 403 }); - } } + } else if (action === 'read' && protocol === 'hls') { + if (password === process.env.MEDIAMTX_PUBLISH_KEY) { + return new Response('authorized', { status: 200 }); + } + const sessionExists = await redis.exists(`sessions:${password}`); + if (!sessionExists) { + return new Response('unauthorized', { status: 401 }); + } + return new Response('authorized', { status: 200 }); } - - return new Response('Request processed', { status: 200 }); + return new Response('uhh', { status: 401 }); } const schema = z.object({ diff --git a/apps/web/src/components/app/StreamPlayer/StreamPlayer.tsx b/apps/web/src/components/app/StreamPlayer/StreamPlayer.tsx index 955afbe..c706943 100644 --- a/apps/web/src/components/app/StreamPlayer/StreamPlayer.tsx +++ b/apps/web/src/components/app/StreamPlayer/StreamPlayer.tsx @@ -1,6 +1,7 @@ 'use client'; import { useParams } from 'next/navigation'; +import { useRef, useEffect } from 'react'; import { MediaController, MediaLoadingIndicator, @@ -13,51 +14,40 @@ import { MediaFullscreenButton, } from 'media-chrome/react'; import HlsVideo from 'hls-video-element/react'; +import { useSession } from '@/lib/providers/SessionProvider'; export default function StreamPlayer() { const { username } = useParams(); + const { session } = useSession(); + const videoRef = useRef(null); + + useEffect(() => { + if (videoRef.current && username) { + const user = 'skibiditoilet'; + const credentials = btoa(`${user}:${session!.id}`); + + // @ts-ignore + videoRef.current.config = { + xhrSetup: (xhr: XMLHttpRequest) => { + xhr.setRequestHeader('Authorization', `Basic ${credentials}`); + xhr.setRequestHeader('hi', 'afjlkafbjadlkfghbjlk'); + }, + lowLatencyMode: true, + debug: process.env.NODE_ENV === 'development', + }; + + // @ts-ignore + videoRef.current.src = `${process.env.NEXT_PUBLIC_MEDIAMTX_URL}/${username}/index.m3u8`; + } + }, [username]); return ( { - const user = 'skibiditoilet'; - const pass = getCookie('auth_session'); - const credentials = btoa(`${user}:${pass}`); - xhr.setRequestHeader('Authorization', `Basic ${credentials}`); - }, - lowLatencyMode: true, - liveSyncDurationCount: 1, - liveMaxLatencyDurationCount: 2, - liveDurationInfinity: true, - enableWorker: true, - backBufferLength: 1, - startLevel: -1, - maxBufferLength: 2, - maxMaxBufferLength: 4, - startFragPrefetch: true, - testBandwidth: false, - progressive: false, - maxBufferSize: 10 * 1000 * 1000, - maxBufferHole: 0.1, - highBufferWatchdogPeriod: 0.5, - nudgeOffset: 0.01, - nudgeMaxRetry: 3, - manifestLoadingTimeOut: 3000, - manifestLoadingMaxRetry: 3, - levelLoadingTimeOut: 3000, - fragLoadingTimeOut: 5000, - debug: process.env.NODE_ENV === 'development', - liveSyncDuration: 1, - liveMaxLatencyDuration: 3, - maxLiveSyncPlaybackRate: 1.5, - liveBackBufferLength: 0, - }} /> @@ -73,10 +63,3 @@ export default function StreamPlayer() { ); } - -function getCookie(name: string): string | null { - const value = `; ${document.cookie}`; - const parts = value.split(`; ${name}=`); - if (parts.length === 2) return parts.pop()!.split(';').shift() || null; - return null; -} diff --git a/apps/web/src/lib/instrumentation/streamInfo.ts b/apps/web/src/lib/instrumentation/streamInfo.ts index 2e2bca0..f1a6227 100644 --- a/apps/web/src/lib/instrumentation/streamInfo.ts +++ b/apps/web/src/lib/instrumentation/streamInfo.ts @@ -2,6 +2,7 @@ import { prisma } from '@hctv/db'; import { HttpFlv } from '../types/liveBackendJson'; import { getNotificationQueue } from '../workers'; import client from '../services/slackNotifier'; +import type { paths } from '../types/mediamtx.d.ts'; export default async function runner() { // if there are no users it explodes so yeah @@ -48,28 +49,21 @@ export async function initializeStreamInfo(channelId?: string) { export async function syncStream() { try { - const response = await fetch(`${process.env.LIVE_SERVER_URL}/stat`, { - headers: { - Authorization: process.env.STAT_AUTH!, - }, - }); + const response = await fetch(`${process.env.MEDIAMTX_API}/v3/paths/list?itemsPerPage=1000`); if (!response.ok) { console.error(`Failed to fetch stream stats: ${response.status} ${response.statusText}`); return; } - const data = await response.json(); - const httpFlv = data['http-flv'] as HttpFlv; + type ResponseType = paths['/v3/paths/list']['get']['responses']['200']['content']['application/json']; + const data = await response.json() as ResponseType; - if (!httpFlv?.servers?.[0]?.applications) { + if (!data) { return; } - const channelLiveApp = httpFlv.servers[0].applications.find( - (app) => app.name === 'channel-live' - ); - const activeStreams = channelLiveApp?.live?.streams || []; + const activeStreams = data.items!; const currentLiveStreams = await prisma.streamInfo.findMany({ where: { isLive: true }, @@ -78,8 +72,7 @@ export async function syncStream() { const activeStreamMap = new Map(); for (const stream of activeStreams) { activeStreamMap.set(stream.name, { - isLive: stream.active, - viewers: stream.clients.filter((c) => !c.publishing).length, + isLive: stream.ready, }); } @@ -99,7 +92,7 @@ export async function syncStream() { } for (const stream of activeStreams) { - if (stream.active) { + if (stream.ready) { const existingStream = await prisma.streamInfo.findUnique({ where: { username: stream.name }, include: { channel: true }, diff --git a/apps/web/src/lib/types/mediamtx.d.ts b/apps/web/src/lib/types/mediamtx.d.ts index 816eed1..9be401a 100644 --- a/apps/web/src/lib/types/mediamtx.d.ts +++ b/apps/web/src/lib/types/mediamtx.d.ts @@ -1,8 +1,6 @@ /** * This file was auto-generated by openapi-typescript. * Do not make direct changes to the file. - * - * committing because i cba to generate this on ci yet */ export interface paths { diff --git a/apps/web/src/lib/workers/worker/thumbnails.ts b/apps/web/src/lib/workers/worker/thumbnails.ts index d2ddefe..11fcaa0 100644 --- a/apps/web/src/lib/workers/worker/thumbnails.ts +++ b/apps/web/src/lib/workers/worker/thumbnails.ts @@ -1,9 +1,9 @@ import { Worker } from 'bullmq'; import { getRedisConnection } from '@hctv/db'; -import { exec } from 'node:child_process'; import { promisify } from 'node:util'; import { existsSync } from 'node:fs'; -const pExec = promisify(exec); +import { exec as execCallback } from 'node:child_process'; +const pExec = promisify(execCallback); const globalForWorker = global as unknown as { thumbnailWorker: Worker | null; @@ -26,28 +26,24 @@ export async function registerThumbnailWorker(): Promise { try { // this is totally unnecessary, but i'll keep it for security purposes. const name = job.data.name.replace(/[^a-zA-Z0-9]/g, '_'); - const m3u8location = `/dev/shm/hls/${name}.m3u8`; + const m3u8location = `${process.env.NEXT_PUBLIC_MEDIAMTX_URL}/${name}/index.m3u8`; const thumbDir = '/dev/shm/hctv-thumb'; - if (!existsSync(m3u8location)) return; if (!existsSync(thumbDir)) { await pExec(`mkdir -p ${thumbDir}`); } - // unnecessary for development, but maybe docker volumes mess with permissions in prod - // also ik it's not the best practice to use 777, but it'll be fiiiiiine - // await pExec('chown -R 777 /dev/shm/hctv-thumb'); - exec( - `ffmpeg -i ${m3u8location} -vframes 1 -an -y -f image2 ${thumbDir}/${name}.webp`, - (error) => { - if (error) { - console.error(`Error: ${error.message}`); - return { success: false, error: error.message }; - } - } - ); - - return { success: true }; + const header = `-headers "Authorization: Basic ${Buffer.from(`skibiditoilet:${process.env.MEDIAMTX_PUBLISH_KEY}`).toString('base64')}\r\n" `; + + try { + await pExec( + `ffmpeg ${header} -i ${m3u8location} -vframes 1 -an -y -f image2 ${thumbDir}/${name}.webp` + ); + return { success: true }; + } catch (ffmpegError) { + console.error(`FFmpeg error for ${name}:`, ffmpegError); + return { success: false, error: ffmpegError instanceof Error ? ffmpegError.message : String(ffmpegError) }; + } } catch (e) { console.error('Slack notification failed:', e); // @ts-ignore e is unknown diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index 2693651..56d4614 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -20,5 +20,6 @@ services: ports: - 8890:8890/udp - 8891:8888 + - 9997:9997 volumes: - ./mediamtx.yml:/mediamtx.yml \ No newline at end of file diff --git a/dev/mediamtx.yml b/dev/mediamtx.yml index 933a37e..713b91d 100644 --- a/dev/mediamtx.yml +++ b/dev/mediamtx.yml @@ -8,4 +8,6 @@ srtAddress: :8890 hls: yes authMethod: http -authHTTPAddress: http://192.168.1.47:3000/api/mediamtx/publish \ No newline at end of file +authHTTPAddress: http://192.168.1.47:3000/api/mediamtx/publish + +api: yes \ No newline at end of file