import { Component, createSignal, createEffect, For, onCleanup, onMount, Show } from 'solid-js'
import { Link, useNavigate } from '@tanstack/solid-router'
import {
ArrowRight,
Cloud,
Hash,
MonitorPlay,
RadioTower,
Server,
ShieldCheck,
Video,
} from 'lucide-solid'
import { api } from '../../lib/api'
import { permissions } from '../../lib/permissions'
import {
Badge,
Button,
Corners,
LiveDot,
Marquee,
Meter,
Reveal,
} from '../../components/ui'
import type { BackendInfo, Room } from '../../lib/types'
const PROTOCOLS = ['WHIP', 'WHEP', 'HLS', 'FLV']
function backendIcon(kind: string) {
return kind === 'BACKEND_KIND_CLOUDFLARE' ? Cloud : Server
}
function targetsOf(r: Room) {
return r.targets?.length ?? 0
}
function EmptyRooms(props: { authed: boolean }) {
return (
{props.authed ? '暂无在线房间' : '房间列表需登录'}
{props.authed
? '创建一个房间后,分发拓扑会出现在这里。'
: '登录后可查看与管理房间分发拓扑。'}
去登录
)
}
export const Home: Component = () => {
const navigate = useNavigate()
const [backends, setBackends] = createSignal([])
const [rooms, setRooms] = createSignal([])
const [room, setRoom] = createSignal('demo')
let heroEl: HTMLElement | undefined
onMount(() => {
api
.getConfig()
.then((c) => setBackends(c.backends || []))
.catch(() => {})
const el = heroEl
if (el && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
// 鼠标视差:各层按 data-parallax 深度位移,rAF + lerp 平滑;聚光灯同步跟随
const layers = Array.from(el.querySelectorAll('[data-parallax]'))
let tx = 0
let ty = 0
let cx = 0
let cy = 0
let raf = 0
const onMove = (e: MouseEvent) => {
const r = el.getBoundingClientRect()
tx = (e.clientX - r.left) / r.width - 0.5
ty = (e.clientY - r.top) / r.height - 0.5
el.style.setProperty('--mx', `${e.clientX - r.left}px`)
el.style.setProperty('--my', `${e.clientY - r.top}px`)
}
const onLeave = () => {
tx = 0
ty = 0
}
const tick = () => {
cx += (tx - cx) * 0.08
cy += (ty - cy) * 0.08
for (const layer of layers) {
const d = Number(layer.dataset.parallax || 0)
layer.style.transform = `translate3d(${(cx * d).toFixed(2)}px, ${(cy * d).toFixed(2)}px, 0)`
}
raf = requestAnimationFrame(tick)
}
el.addEventListener('mousemove', onMove)
el.addEventListener('mouseleave', onLeave)
raf = requestAnimationFrame(tick)
onCleanup(() => {
el.removeEventListener('mousemove', onMove)
el.removeEventListener('mouseleave', onLeave)
cancelAnimationFrame(raf)
})
}
})
// 登录态就绪后再拉房间(整页刷新时鉴权接口晚于组件挂载,需响应式等待)
createEffect(() => {
if (!permissions.check()) return
if (!permissions.isAuthed()) return
api
.listRooms()
.then((r) => setRooms(r.rooms || []))
.catch(() => {})
})
const go = (to: string) => navigate({ to, search: { room: room() } })
return (
{/* ===== HERO ===== */}
(heroEl = n)} class="relative overflow-hidden">
SFU
live-sfu / fan-out console
一路推流,
多端扇出
/ fan-out
Cloudflare Realtime 与 SRS 双 SFU 分发,浏览器经服务端反代直连,凭证不出服务端。
{/* ===== 在线房间 + 信号栈 ===== */}
0} fallback={}>
{(r, i) => (
{String(i() + 1).padStart(2, '0')}
0} />
{r.name}
{targetsOf(r)} targets · {targetsOf(r) > 0 ? 'live' : 'idle'}
)}
)
}