import { Component, createSignal, onCleanup, onMount, Show, For } from 'solid-js' import { useSearch, useNavigate } from '@tanstack/solid-router' import { Cloud, Eye, EyeOff, FileVideo, Link2, MessageSquare, MonitorPlay, Radio, Square, Video, } from 'lucide-solid' import { api } from '../../lib/api' import { permissions } from '../../lib/permissions' import { Alert, Badge, Button, Card, LiveDot, 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 { RoomEvent } from '../../lib/types' type Mode = 'cloudflare' | 'srs-whep' | 'srs-hls' const MODE_LABEL: Record = { cloudflare: 'Cloudflare', 'srs-whep': 'SRS WHEP', 'srs-hls': 'SRS HLS', } function backendText(kind: string) { return kind === 'BACKEND_KIND_CLOUDFLARE' ? 'Cloudflare Realtime' : 'SRS' } function RoomHub(props: { room: string }) { const navigate = useNavigate() const room = () => props.room const [mode, setMode] = createSignal('cloudflare') const [running, setRunning] = createSignal(false) const [error, setError] = createSignal('') const [targets, setTargets] = 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 || [])) 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() === 'cloudflare') { 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 { 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 (

房间 {room()}

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

{error()}
setMode(v)} options={[ { value: 'cloudflare', label: 'Cloudflare', icon: }, { value: 'srs-whep', label: 'SRS WHEP', icon: }, { value: 'srs-hls', label: 'SRS HLS', icon: }, ]} />

弹幕 / 聊天

{msgs().length}

分发状态

0} fallback={
房间暂无直播
} >
{(t) => (
{backendText(t.backend)} 直播中

session:{t.session_id}

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