import type { LivestreamStore } from "@streamplace/core"; import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useStore } from "zustand"; import { useShallow } from "zustand/react/shallow"; import { Player } from "../../components/player/player"; import { useFullscreen } from "../../contexts/fullscreen-context"; import type { Liveness } from "../../hooks/use-liveness-state"; import { captureError } from "../../lib/log"; import { useStore as useAppStore } from "../../lib/store"; import { getStreamplaceUrl } from "../../lib/streamplace-url"; import { DanmuOverlay } from "./danmu-overlay"; import { PlayerOffline } from "./player-offline"; import { UserOffline } from "./user-offline"; const DANMU_KEY = "danmu-enabled"; function readDanmuPreference(): boolean { try { return localStorage.getItem(DANMU_KEY) === "true"; } catch { return false; } } function writeDanmuPreference(enabled: boolean) { try { localStorage.setItem(DANMU_KEY, String(enabled)); } catch { // ignore } } type Segment = { width: number; height: number } | null; type Problem = { code: string; severity: string; message: string }; export function VideoSection({ store, user, liveness, }: { store: LivestreamStore; user: string; liveness: Liveness; }) { const [showDanmu, setShowDanmu] = useState(readDanmuPreference); const handleDanmuChange = useCallback((enabled: boolean) => { setShowDanmu(enabled); writeDanmuPreference(enabled); }, []); // Danmu tuning preferences from the shared store (settings page). const danmuOpacity = useAppStore((s) => s.danmuOpacity); const danmuSpeed = useAppStore((s) => s.danmuSpeed); const danmuLaneCount = useAppStore((s) => s.danmuLaneCount); const danmuMaxMessages = useAppStore((s) => s.danmuMaxMessages); const state = useStore( store, useShallow((s) => ({ livestream: s.livestream, problems: s.problems, websocketConnected: s.websocketConnected, segment: s.segment, })), ); const { playlistUrl, thumbnailUrl } = useMemo(() => { const base = getStreamplaceUrl(); return { playlistUrl: `${base}/xrpc/place.stream.playback.getLivePlaylist?streamer=${encodeURIComponent(user)}`, thumbnailUrl: `${base}/api/playback/${encodeURIComponent(user)}/stream.jpg`, }; }, [user]); return ( ); } /** * Props-based video section that works without a LivestreamStore. * Used by VideoSection (live) and directly by VOD pages. */ export function VideoSectionInner({ user, liveness, segment, problems, playlistUrl, thumbnailUrl, mode = "live", store, showDanmu = false, onShowDanmuChange, danmuOpacity = 80, danmuSpeed = 1, danmuLaneCount = 12, danmuMaxMessages = 50, }: { user: string; liveness: Liveness; segment: Segment; problems: Problem[]; playlistUrl: string; thumbnailUrl: string; mode?: "live" | "vod"; store?: LivestreamStore; showDanmu?: boolean; onShowDanmuChange?: (show: boolean) => void; danmuOpacity?: number; danmuSpeed?: number; danmuLaneCount?: number; danmuMaxMessages?: number; }) { const { t } = useTranslation("common"); const { theatre } = useFullscreen(); const neverLive = liveness === "never-live"; const offline = liveness === "offline"; // Show the live player when neither never-live nor offline. This // includes the initial loading state before the WebSocket sends its // first snapshot, plus live and temporarily stale streams. const showPlayer = !neverLive && !offline; // calculate seg ratio for poster aspect ratio correction const segRatio = segment ? segment.width / segment.height : 16 / 9; return (
{neverLive ? ( { (e.currentTarget as HTMLImageElement).style.visibility = "hidden"; }} /> ) : ( <> {/* Live player fades out when the stream goes offline. */}
captureError(message, { user, mode })} danmuOverlay={ store ? ( ) : undefined } />
{offline && store && (
)} )} {liveness === "stale" && (

{t("reconnecting")}

{t("stream-may-resume")}

)} {neverLive && } {showPlayer && problems .filter((p) => p.severity === "error") .map((p) => (
{p.message}
))}
); }