diff --git a/packages/web/src/env.d.ts b/packages/web/src/env.d.ts index 9887dff3..96aa6664 100644 --- a/packages/web/src/env.d.ts +++ b/packages/web/src/env.d.ts @@ -13,6 +13,7 @@ declare namespace Cloudflare { OPENROUTER_MODEL?: string GEMINI_API_KEY?: string FLOWGLAD_SECRET_KEY?: string + CLOUDFLARE_STREAM_NIKIV_VIDEO_ID?: string } } diff --git a/packages/web/src/routes/$username.tsx b/packages/web/src/routes/$username.tsx index d500da4c..fcd920b4 100644 --- a/packages/web/src/routes/$username.tsx +++ b/packages/web/src/routes/$username.tsx @@ -22,31 +22,32 @@ export const Route = createFileRoute("/$username")({ component: StreamPage, }) -// Cloudflare Stream HLS URL -const HLS_URL = "https://customer-xctsztqzu046isdc.cloudflarestream.com/1b0363e3f8d54ddc639dc85737f8c28a/manifest/video.m3u8" -const NIKIV_PLAYBACK = resolveStreamPlayback({ hlsUrl: HLS_URL, webrtcUrl: null }) +// Default Cloudflare Stream HLS URL (will be overridden by API) +const DEFAULT_HLS_URL = "https://customer-xctsztqzu046isdc.cloudflarestream.com/cd56ef73791c628c252cd290ee710275/manifest/video.m3u8" const READY_PULSE_MS = 1200 -// Hardcoded user for nikiv -const NIKIV_DATA: StreamPageData = { - user: { - id: "nikiv", - name: "Nikita", - username: "nikiv", - image: null, - }, - stream: { - id: "nikiv-stream", - title: "Live Coding", - description: "Building in public", - is_live: false, // Set to true when actually streaming - viewer_count: 0, - hls_url: HLS_URL, - webrtc_url: null, - playback: NIKIV_PLAYBACK, - thumbnail_url: null, - started_at: null, - }, +// Hardcoded user for nikiv (hls_url will be updated from API) +function makeNikivData(hlsUrl: string): StreamPageData { + return { + user: { + id: "nikiv", + name: "Nikita", + username: "nikiv", + image: null, + }, + stream: { + id: "nikiv-stream", + title: "Live Coding", + description: "Building in public", + is_live: false, + viewer_count: 0, + hls_url: hlsUrl, + webrtc_url: null, + playback: resolveStreamPlayback({ hlsUrl, webrtcUrl: null }), + thumbnail_url: null, + started_at: null, + }, + } } function StreamPage() { @@ -57,6 +58,7 @@ function StreamPage() { const [error, setError] = useState(null) const [playerReady, setPlayerReady] = useState(false) const [hlsLive, setHlsLive] = useState(null) + const [hlsUrl, setHlsUrl] = useState(DEFAULT_HLS_URL) const [isConnecting, setIsConnecting] = useState(false) const [nowPlaying, setNowPlaying] = useState( null, @@ -76,9 +78,9 @@ function StreamPage() { useEffect(() => { let isActive = true - // Special handling for nikiv - hardcoded stream + // Special handling for nikiv - hardcoded stream with dynamic HLS URL if (username === "nikiv") { - setData(NIKIV_DATA) + setData(makeNikivData(hlsUrl)) setLoading(false) return () => { isActive = false @@ -193,9 +195,15 @@ function StreamPage() { const res = await fetch("/api/check-hls", { cache: "no-store" }) if (!isActive) return - const data = await res.json() + const apiData = await res.json() - if (data.isLive) { + // Update HLS URL if returned from API + if (apiData.hlsUrl && apiData.hlsUrl !== hlsUrl) { + setHlsUrl(apiData.hlsUrl) + setData(makeNikivData(apiData.hlsUrl)) + } + + if (apiData.isLive) { // Stream is live - set connecting state if first time if (!hasConnectedOnce.current) { setIsConnecting(true) @@ -224,7 +232,7 @@ function StreamPage() { isActive = false clearInterval(interval) } - }, [username]) + }, [username, hlsUrl]) // For non-nikiv users, use direct HLS check useEffect(() => { diff --git a/packages/web/src/routes/api/check-hls.ts b/packages/web/src/routes/api/check-hls.ts index 45d30648..c823910a 100644 --- a/packages/web/src/routes/api/check-hls.ts +++ b/packages/web/src/routes/api/check-hls.ts @@ -1,4 +1,4 @@ -import { createFileRoute } from "@tanstack/react-router" +import { createFileRoute, getServerContext } from "@tanstack/react-router" const json = (data: unknown, status = 200) => new Response(JSON.stringify(data), { @@ -6,8 +6,14 @@ const json = (data: unknown, status = 200) => headers: { "content-type": "application/json" }, }) -// Cloudflare Stream HLS URL -const HLS_URL = "https://customer-xctsztqzu046isdc.cloudflarestream.com/1b0363e3f8d54ddc639dc85737f8c28a/manifest/video.m3u8" +// Default video ID (fallback) +const DEFAULT_VIDEO_ID = "cd56ef73791c628c252cd290ee710275" + +function getHlsUrl(): string { + const ctx = (getServerContext as () => { cloudflare?: { env?: Record } } | null)() + const videoId = ctx?.cloudflare?.env?.CLOUDFLARE_STREAM_NIKIV_VIDEO_ID || DEFAULT_VIDEO_ID + return `https://customer-xctsztqzu046isdc.cloudflarestream.com/${videoId}/manifest/video.m3u8` +} function isHlsPlaylistLive(manifest: string): boolean { const upper = manifest.toUpperCase() @@ -29,7 +35,8 @@ export const Route = createFileRoute("/api/check-hls")({ handlers: { GET: async () => { try { - const res = await fetch(HLS_URL, { + const hlsUrl = getHlsUrl() + const res = await fetch(hlsUrl, { cache: "no-store", }) @@ -38,6 +45,7 @@ export const Route = createFileRoute("/api/check-hls")({ if (!res.ok) { return json({ isLive: false, + hlsUrl, status: res.status, error: "HLS not available", }) @@ -54,6 +62,7 @@ export const Route = createFileRoute("/api/check-hls")({ return json({ isLive, + hlsUrl, status: res.status, manifestLength: manifest.length, }) @@ -62,6 +71,7 @@ export const Route = createFileRoute("/api/check-hls")({ console.error("[check-hls] Error:", error.message) return json({ isLive: false, + hlsUrl: getHlsUrl(), error: error.message, }) } diff --git a/packages/web/wrangler.jsonc b/packages/web/wrangler.jsonc index 665c6ea6..192113d7 100644 --- a/packages/web/wrangler.jsonc +++ b/packages/web/wrangler.jsonc @@ -29,7 +29,8 @@ * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables */ "vars": { - "APP_BASE_URL": "https://linsa.io" + "APP_BASE_URL": "https://linsa.io", + "CLOUDFLARE_STREAM_NIKIV_VIDEO_ID": "cd56ef73791c628c252cd290ee710275" }, /** * Note: Use secrets to store sensitive data.