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