import { Component, createSignal, onCleanup, onMount, Show, For } from 'solid-js' import { useSearch, useNavigate } from '@tanstack/solid-router' import { Cloud, Eye, EyeOff, FileVideo, Globe, MessageSquare, MonitorPlay, Radio, Square, Video, } from 'lucide-solid' import { api } from '../../lib/api' import { permissions } from '../../lib/permissions' import { Alert, Badge, Button, Corners, LiveDot, Reveal, Segmented } from '../../components/ui' import { subscribeRoom } from '../../lib/sse' import { whepSubscribe, cfSubscribe, toRTC } from '../../lib/webrtc' import { playHLS } from '../../lib/hls' import { subscribeChat, sendChat, type ChatMessage } from '../../lib/chat' import { DanmakuOverlay, ChatPanel } from '../../components/danmaku' import type { DistributionTarget, RoomEvent } from '../../lib/types' type Mode = 'srs-hls' | 'srs-whep' | 'cf-sfu' | 'cdn-hls' const MODE_LABEL: Record = { 'srs-hls': 'SRS HLS', 'srs-whep': 'SRS WHEP', 'cf-sfu': 'CF SFU', 'cdn-hls': 'CDN HLS', } function distText(kind: string) { if (kind === 'DISTRIBUTION_KIND_SRS_HLS') return 'SRS HLS' if (kind === 'DISTRIBUTION_KIND_CF_SFU') return 'CF SFU' if (kind === 'DISTRIBUTION_KIND_CDN') return 'CDN' return kind } function RoomHub(props: { room: string }) { const navigate = useNavigate() const room = () => props.room const [mode, setMode] = createSignal('srs-hls') const [running, setRunning] = createSignal(false) const [error, setError] = createSignal('') const [targets, setTargets] = createSignal([]) const [dists, setDists] = createSignal([]) const [msgs, setMsgs] = createSignal([]) const [showDanmaku, setShowDanmaku] = createSignal(true) let videoEl: HTMLVideoElement | undefined let pc: RTCPeerConnection | undefined let stopHls: (() => void) | undefined onMount(() => { const unsubRoom = subscribeRoom(room(), (ev) => { setTargets(ev.targets || []) setDists(ev.distributions || []) }) const unsubChat = subscribeChat(room(), (m) => setMsgs((p) => [...p, m].slice(-100))) onCleanup(() => { unsubRoom() unsubChat() }) }) function attach(p: RTCPeerConnection) { p.ontrack = (e) => { if (videoEl) videoEl.srcObject = e.streams[0] } } const stopAll = () => { stopHls?.() stopHls = undefined pc?.close() pc = undefined if (videoEl) { videoEl.srcObject = null videoEl.src = '' } setRunning(false) } onCleanup(stopAll) const start = async () => { setError('') setRunning(true) try { if (mode() === 'cf-sfu') { const resp = await api.subscribe(room(), 'BACKEND_KIND_CLOUDFLARE', '') pc = await cfSubscribe({ viewerSessionId: resp.session_id!, publisherSessionId: resp.publisher_session_id!, iceServers: (resp.ice_servers || []).map(toRTC), }) attach(pc) } else if (mode() === 'srs-whep') { const resp = await api.subscribe(room(), 'BACKEND_KIND_SRS', '') pc = await whepSubscribe({ stream: resp.stream!, iceServers: (resp.ice_servers || []).map(toRTC) }) attach(pc) } else if (mode() === 'cdn-hls') { const resp = await api.subscribe(room(), 'BACKEND_KIND_SRS', '') if (!videoEl) throw new Error('video element missing') const cdnDist = dists().find((d) => d.kind === 'DISTRIBUTION_KIND_CDN') // Prefer CDN URL if available, fallback to /live/cdn path const url = cdnDist?.url || `/live/cdn/${resp.stream}.m3u8` stopHls = playHLS(videoEl, url) } else { const resp = await api.subscribe(room(), 'BACKEND_KIND_SRS', '') if (!videoEl) throw new Error('video element missing') stopHls = playHLS(videoEl, `/live/${resp.stream}.m3u8`) } } catch (e: any) { setError(String(e?.message || e)) setRunning(false) } } const send = (text: string) => { sendChat(room(), text).catch(() => {}) } return (

watch / room

{room()}

观看、发弹幕与查看分发状态。

0} /> {targets().length} trunk · {dists().length} 分发 · {targets().length > 0 ? 'live' : 'idle'}
{error()}
setMode(v)} options={[ { value: 'srs-hls', label: 'SRS HLS', icon: }, { value: 'srs-whep', label: 'SRS WHEP', icon: }, { value: 'cf-sfu', label: 'CF SFU', icon: }, { value: 'cdn-hls', label: 'CDN HLS', icon: }, ]} />

弹幕 / 聊天

{msgs().length}

分发状态

fan-out status
0 || dists().length > 0} fallback={
房间暂无直播
} >
0}>
{(t) => (
)}
0}>
{(d) => (
{distText(d.kind)} {d.status}

{d.url}

)}
) } function PlayIcon() { return ( ) } export const WatchRoute: Component = () => { const search = useSearch({ from: '/watch' }) return } export const RoomRoute: Component = () => { const search = useSearch({ from: '/room' }) return }