From 953bc38c12d9c653feabd05c71ceb3018f8caa5d Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Wed, 29 Apr 2026 16:14:48 +0200 Subject: [PATCH] feat(bs): production prepping --- apps/web/.env.example | 5 +- .../(protected)/api/mediamtx/publish/route.ts | 7 +++ .../src/app/(ui)/(protected)/stream/page.tsx | 6 +- apps/web/src/components/app/NavBar/NavBar.tsx | 23 ++++++- .../web/src/lib/instrumentation/streamInfo.ts | 10 ++- apps/web/src/lib/utils/mediamtx/client.ts | 12 ++++ apps/web/src/lib/utils/mediamtx/regions.ts | 2 +- apps/web/src/lib/utils/mediamtx/server.ts | 12 ++++ docker/mediamtx/mediamtx.yml | 6 +- docker/mediamtx/mirror/.env.example | 12 ++++ docker/mediamtx/mirror/docker-compose.yml | 63 +++++++++++++++++++ docker/mediamtx/mirror/mediamtx.yml | 26 ++++++++ 12 files changed, 177 insertions(+), 7 deletions(-) create mode 100644 docker/mediamtx/mirror/.env.example create mode 100644 docker/mediamtx/mirror/docker-compose.yml create mode 100644 docker/mediamtx/mirror/mediamtx.yml diff --git a/apps/web/.env.example b/apps/web/.env.example index 84b2f95..ae709a0 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -28,5 +28,6 @@ NEXT_PUBLIC_MEDIAMTX_WHIP_ROUTE_HQ=http://localhost:8889 # MEDIAMTX_API_ASIA=http://localhost:9999 # NEXT_PUBLIC_MEDIAMTX_INGEST_ROUTE_ASIA=localhost:8990 -# idt you should change this -MEDIAMTX_PUBLISH_KEY=rjq1xdpCPA4qyt3jge \ No newline at end of file +# generate with `openssl rand -base64 20` +MEDIAMTX_PUBLISH_KEY= +MEDIAMTX_API_KEY= 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 e9b4083..919aff2 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 @@ -80,6 +80,13 @@ export async function POST(request: NextRequest) { } return finish('authorized', 200, 'authorized_read'); } + if (parsedAction === 'api') { + if (password === process.env.MEDIAMTX_API_KEY) { + return finish('authorized api', 200, 'authorized_api'); + } + + return finish('unauthorized api', 401, 'unauthorized_api'); + } return finish('uhh', 401, 'unauthorized'); } diff --git a/apps/web/src/app/(ui)/(protected)/stream/page.tsx b/apps/web/src/app/(ui)/(protected)/stream/page.tsx index c6b5094..524aefc 100644 --- a/apps/web/src/app/(ui)/(protected)/stream/page.tsx +++ b/apps/web/src/app/(ui)/(protected)/stream/page.tsx @@ -259,7 +259,11 @@ export default function Page() { {serverOptions.map((server) => ( - + {server.label} {server.emoji} ))} diff --git a/apps/web/src/components/app/NavBar/NavBar.tsx b/apps/web/src/components/app/NavBar/NavBar.tsx index 102a2d5..1914bfb 100644 --- a/apps/web/src/components/app/NavBar/NavBar.tsx +++ b/apps/web/src/components/app/NavBar/NavBar.tsx @@ -15,7 +15,18 @@ import { logout } from '@/lib/auth/actions'; import { useSession } from '@/lib/providers/SessionProvider'; import Link from 'next/link'; import { ThemeSwitcher } from '../ThemeSwitcher/ThemeSwitcher'; -import { IdCard, Shield, Settings, Users, PenSquare, LogOut, Code, Github, Heart } from 'lucide-react'; +import { + IdCard, + Shield, + Settings, + Users, + PenSquare, + LogOut, + Code, + Github, + Heart, + Radio, +} from 'lucide-react'; import { SidebarTrigger } from '@/components/ui/sidebar'; import Image from 'next/image'; import Logo from '@/lib/assets/logo.webp'; @@ -52,6 +63,16 @@ export default function Navbar(props: Props) { {/* Right Side Items */}
+ {user && ( + + + + )} + {props.editLivestream &&
{props.editLivestream}
} {user ? ( diff --git a/apps/web/src/lib/instrumentation/streamInfo.ts b/apps/web/src/lib/instrumentation/streamInfo.ts index 32684dd..39bdecf 100644 --- a/apps/web/src/lib/instrumentation/streamInfo.ts +++ b/apps/web/src/lib/instrumentation/streamInfo.ts @@ -90,7 +90,15 @@ export async function syncStream() { for (const r of regions) { const region = MEDIAMTX_SERVER_REGIONS[r]; - const response = await fetch(`${region.apiUrl}/v3/paths/list?itemsPerPage=1000`); + if (!region.apiAuthHeader) { + throw new Error('MEDIAMTX_API_KEY is required when querying the MediaMTX API'); + } + + const response = await fetch(`${region.apiUrl}/v3/paths/list?itemsPerPage=1000`, { + headers: { + Authorization: region.apiAuthHeader, + }, + }); if (!response.ok) { recordStreamSyncScrape(r, 'error'); diff --git a/apps/web/src/lib/utils/mediamtx/client.ts b/apps/web/src/lib/utils/mediamtx/client.ts index 5e623f0..edef96d 100644 --- a/apps/web/src/lib/utils/mediamtx/client.ts +++ b/apps/web/src/lib/utils/mediamtx/client.ts @@ -5,6 +5,7 @@ export interface MediaMTXClientEnvs { publicUrl: string; ingestRoute: string; whip: string; + whipEnabled: boolean; emoji: string; string: string; } @@ -13,6 +14,7 @@ export interface MediaMTXClientRegionOption { value: MediaMTXRegion; emoji: string; label: string; + whipEnabled: boolean; } export function getMediamtxClientEnvs(region: MediaMTXRegion = 'hq'): MediaMTXClientEnvs { @@ -21,9 +23,18 @@ export function getMediamtxClientEnvs(region: MediaMTXRegion = 'hq'): MediaMTXCl publicUrl: getEnv('NEXT_PUBLIC_MEDIAMTX_URL_HQ')!, ingestRoute: getEnv('NEXT_PUBLIC_MEDIAMTX_INGEST_ROUTE_HQ')!, whip: getEnv('NEXT_PUBLIC_MEDIAMTX_WHIP_ROUTE_HQ')!, + whipEnabled: false, emoji: 'πŸ‡ΊπŸ‡Έ', string: 'HQ Server A', }, + ethande: { + publicUrl: getEnv('NEXT_PUBLIC_MEDIAMTX_URL_ETHANDE')!, + ingestRoute: getEnv('NEXT_PUBLIC_MEDIAMTX_INGEST_ROUTE_ETHANDE')!, + whip: getEnv('NEXT_PUBLIC_MEDIAMTX_WHIP_ROUTE_ETHANDE')!, + whipEnabled: true, + emoji: 'πŸ‡©πŸ‡ͺ', + string: 'eth0\'s VPS', + }, }; const regionEnvs = envs[region]; @@ -41,6 +52,7 @@ export function getMediamtxClientRegionOptions(): MediaMTXClientRegionOption[] { value: 'hq', emoji: 'πŸ‡ΊπŸ‡Έ', label: 'HQ Server A', + whipEnabled: false, }, ]; } diff --git a/apps/web/src/lib/utils/mediamtx/regions.ts b/apps/web/src/lib/utils/mediamtx/regions.ts index 4140a62..0a3a23a 100644 --- a/apps/web/src/lib/utils/mediamtx/regions.ts +++ b/apps/web/src/lib/utils/mediamtx/regions.ts @@ -1 +1 @@ -export type MediaMTXRegion = 'hq'; +export type MediaMTXRegion = 'hq' | 'ethande'; diff --git a/apps/web/src/lib/utils/mediamtx/server.ts b/apps/web/src/lib/utils/mediamtx/server.ts index 8866c46..73033b2 100644 --- a/apps/web/src/lib/utils/mediamtx/server.ts +++ b/apps/web/src/lib/utils/mediamtx/server.ts @@ -2,11 +2,13 @@ import { MediaMTXRegion } from './regions'; export interface MediaMTXEnvs { apiUrl: string; + apiAuthHeader?: string; } export const MEDIAMTX_SERVER_REGIONS: Record = { hq: { apiUrl: process.env.MEDIAMTX_API_HQ!, + apiAuthHeader: getMediamtxApiAuthHeader(), }, }; @@ -19,3 +21,13 @@ export function getMediamtxEnvs(region: MediaMTXRegion = 'hq'): MediaMTXEnvs { return envs; } + +function getMediamtxApiAuthHeader() { + const apiKey = process.env.MEDIAMTX_API_KEY; + + if (!apiKey) { + return undefined; + } + + return `Basic ${Buffer.from(`hctv-api:${apiKey}`).toString('base64')}`; +} diff --git a/docker/mediamtx/mediamtx.yml b/docker/mediamtx/mediamtx.yml index ebcabd8..284bae8 100644 --- a/docker/mediamtx/mediamtx.yml +++ b/docker/mediamtx/mediamtx.yml @@ -12,10 +12,14 @@ hlsPartDuration: 1s hlsSegmentCount: 10 webrtc: yes +webrtcAddress: :8889 +webrtcLocalUDPAddress: :8189 +webrtcAdditionalHosts: [] authMethod: http -authHTTPAddress: http://hctv:3000/api/mediamtx/publish +authHTTPAddress: https://hackclub.tv/api/mediamtx/publish api: yes +apiAddress: 0.0.0.0:9997 metrics: yes metricsAddress: :9998 diff --git a/docker/mediamtx/mirror/.env.example b/docker/mediamtx/mirror/.env.example new file mode 100644 index 0000000..f0ed859 --- /dev/null +++ b/docker/mediamtx/mirror/.env.example @@ -0,0 +1,12 @@ +ACME_EMAIL=ops@hackclub.tv + +# public hostnames and stuff +MEDIAMTX_HLS_HOST=hls.hackclub.tv +MEDIAMTX_WEBRTC_HOST=whip.hackclub.tv +MEDIAMTX_API_HOST=mmtxapi.hackclub.tv + +# public ip for webrtc stuff +MEDIAMTX_WEBRTC_ADDITIONAL_HOSTS=203.0.113.10 + +# mediamtx publish route on hctv +MEDIAMTX_AUTH_HTTP_ADDRESS=https://hackclub.tv/api/mediamtx/publish diff --git a/docker/mediamtx/mirror/docker-compose.yml b/docker/mediamtx/mirror/docker-compose.yml new file mode 100644 index 0000000..d2371df --- /dev/null +++ b/docker/mediamtx/mirror/docker-compose.yml @@ -0,0 +1,63 @@ +services: + traefik: + image: traefik:v3.5 + command: + - --providers.docker=true + - --providers.docker.exposedbydefault=false + - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 + - --entrypoints.srt.address=:8890/udp + - --entrypoints.webrtc-ice.address=:8189/udp + - --certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL} + - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json + - --certificatesresolvers.letsencrypt.acme.httpchallenge=true + - --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web + ports: + - 80:80 + - 443:443 + - 8890:8890/udp + - 8189:8189/udp + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - letsencrypt:/letsencrypt + restart: unless-stopped + + mediamtx: + image: bluenviron/mediamtx:1 + volumes: + - ./mediamtx.yml:/mediamtx.yml:ro + environment: + MTX_WEBRTCADDITIONALHOSTS: ${MEDIAMTX_WEBRTC_ADDITIONAL_HOSTS} + MTX_AUTHHTTPADDRESS: ${MEDIAMTX_AUTH_HTTP_ADDRESS} + labels: + - traefik.enable=true + + - traefik.http.routers.mediamtx-hls.rule=Host(`${MEDIAMTX_HLS_HOST}`) + - traefik.http.routers.mediamtx-hls.entrypoints=websecure + - traefik.http.routers.mediamtx-hls.tls.certresolver=letsencrypt + - traefik.http.routers.mediamtx-hls.service=mediamtx-hls + - traefik.http.services.mediamtx-hls.loadbalancer.server.port=8888 + + - traefik.http.routers.mediamtx-webrtc.rule=Host(`${MEDIAMTX_WEBRTC_HOST}`) + - traefik.http.routers.mediamtx-webrtc.entrypoints=websecure + - traefik.http.routers.mediamtx-webrtc.tls.certresolver=letsencrypt + - traefik.http.routers.mediamtx-webrtc.service=mediamtx-webrtc + - traefik.http.services.mediamtx-webrtc.loadbalancer.server.port=8889 + + - traefik.http.routers.mediamtx-api.rule=Host(`${MEDIAMTX_API_HOST}`) + - traefik.http.routers.mediamtx-api.entrypoints=websecure + - traefik.http.routers.mediamtx-api.tls.certresolver=letsencrypt + - traefik.http.routers.mediamtx-api.service=mediamtx-api + - traefik.http.services.mediamtx-api.loadbalancer.server.port=9997 + + - traefik.udp.routers.mediamtx-srt.entrypoints=srt + - traefik.udp.routers.mediamtx-srt.service=mediamtx-srt + - traefik.udp.services.mediamtx-srt.loadbalancer.server.port=8890 + + - traefik.udp.routers.mediamtx-webrtc-ice.entrypoints=webrtc-ice + - traefik.udp.routers.mediamtx-webrtc-ice.service=mediamtx-webrtc-ice + - traefik.udp.services.mediamtx-webrtc-ice.loadbalancer.server.port=8189 + restart: unless-stopped + +volumes: + letsencrypt: diff --git a/docker/mediamtx/mirror/mediamtx.yml b/docker/mediamtx/mirror/mediamtx.yml new file mode 100644 index 0000000..995a86b --- /dev/null +++ b/docker/mediamtx/mirror/mediamtx.yml @@ -0,0 +1,26 @@ +paths: + all: + source: publisher + +srt: yes +srtAddress: :8890 + +hls: yes +hlsVariant: lowLatency +hlsSegmentDuration: 2s +hlsPartDuration: 1s +hlsSegmentCount: 10 + +webrtc: yes +webrtcAddress: :8889 +webrtcLocalUDPAddress: :8189 +webrtcAdditionalHosts: [] + +authMethod: http +authHTTPAddress: https://hackclub.tv/api/mediamtx/publish + +api: yes +apiAddress: :9997 + +metrics: yes +metricsAddress: :9998