"use client"; import { Cable, Copy, Monitor, Radio } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { LiveProcessLiveLogCard } from "@/components/live-product/LiveProcessLiveLogCard"; type ProcessEntry = { pm2: string; script: string; label: string }; type TFn = (zh: string, en: string) => string; function isTiktokProcess(e: ProcessEntry) { return /tiktok/i.test(e.script) || /tiktok/i.test(e.pm2); } type HdmiPreset = { id: string; labelZh: string; labelEn: string; spec: string; hintZh: string; hintEn: string }; const PRESETS: HdmiPreset[] = [ { id: "720p60", labelZh: "720p @ 60Hz", labelEn: "720p @ 60Hz", spec: "1280x720, 59.94/60Hz, RGB/YUV 有限", hintZh: "板端 HDMI 输出与采集卡 EDID 对齐;OBS 画布 1280×720。", hintEn: "Match SoC HDMI timing to capture card EDID; OBS 1280×720.", }, { id: "1080p30", labelZh: "1080p @ 30Hz", labelEn: "1080p @ 30Hz", spec: "1920x1080, 30Hz, 适合 USB 采集带宽紧张场景", hintZh: "USB3 采集常更稳;检查线缆长度与屏蔽。", hintEn: "Often more stable on USB3 capture; check cable quality.", }, { id: "1080p60", labelZh: "1080p @ 60Hz", labelEn: "1080p @ 60Hz", spec: "1920x1080, 60Hz, 高刷转播", hintZh: "需确认采集卡与 SoC 同时支持 1080p60;过热会掉帧。", hintEn: "Requires 1080p60 on both ends; thermal throttling may drop frames.", }, ]; const CHECK_KEY = "live-hub-tiktok-hdmi-check-v1"; const NOTES_KEY = "live-hub-tiktok-hdmi-notes-v1"; type Props = { t: TFn; busy: boolean; entries: ProcessEntry[]; currentProc: string; setCurrentProc: (v: string) => void; urlConfig: string; setUrlConfig: (v: string) => void; onRunProcess: (a: "start" | "stop" | "restart") => void; onSaveUrlConfig: (content?: string) => void | Promise; onCopy: (text: string) => void; fetchJson: (path: string, init?: RequestInit) => Promise; }; export function LiveTiktokHdmiView({ t, busy, entries, currentProc, setCurrentProc, urlConfig, setUrlConfig, onRunProcess, onSaveUrlConfig, onCopy, fetchJson, }: Props) { const tkEntries = useMemo(() => entries.filter(isTiktokProcess), [entries]); const processOptions = tkEntries.length ? tkEntries : entries; const [expanded, setExpanded] = useState("1080p30"); const [selectedPreset, setSelectedPreset] = useState("1080p30"); const [urlReloading, setUrlReloading] = useState(false); const [urlAutoRefresh, setUrlAutoRefresh] = useState(true); const [urlFetchNote, setUrlFetchNote] = useState(null); const urlListDirtyRef = useRef(false); const [hdmiNotes, setHdmiNotes] = useState(""); const [checks, setChecks] = useState>({}); useEffect(() => { urlListDirtyRef.current = false; }, [currentProc]); const pullUrlConfigFromServer = useCallback(async () => { if (!currentProc) return; try { const data = await fetchJson<{ content?: string }>( `/get_url_config?process=${encodeURIComponent(currentProc)}`, ); setUrlConfig(data.content ?? ""); } catch { /* ignore */ } }, [currentProc, fetchJson, setUrlConfig]); useEffect(() => { if (!currentProc || !urlAutoRefresh) return; const id = window.setInterval(() => { if (urlListDirtyRef.current) return; void pullUrlConfigFromServer(); }, 8000); return () => clearInterval(id); }, [currentProc, urlAutoRefresh, pullUrlConfigFromServer]); useEffect(() => { try { const raw = window.localStorage.getItem(CHECK_KEY); if (raw) setChecks(JSON.parse(raw) as Record); } catch { /* ignore */ } try { setHdmiNotes(window.localStorage.getItem(NOTES_KEY) || ""); } catch { /* ignore */ } }, []); const persistChecks = useCallback((next: Record) => { setChecks(next); try { window.localStorage.setItem(CHECK_KEY, JSON.stringify(next)); } catch { /* ignore */ } }, []); const persistNotes = (v: string) => { setHdmiNotes(v); try { window.localStorage.setItem(NOTES_KEY, v); } catch { /* ignore */ } }; const reloadUrlConfig = async () => { if (!currentProc) return; setUrlReloading(true); setUrlFetchNote(null); urlListDirtyRef.current = false; try { const data = await fetchJson<{ content?: string }>( `/get_url_config?process=${encodeURIComponent(currentProc)}`, ); setUrlConfig(data.content ?? ""); setUrlFetchNote(t("已从服务器读取 URL_config", "Reloaded URL_config from server")); } catch (e) { setUrlFetchNote((e as Error).message); } finally { setUrlReloading(false); } }; const saveUrlAndClearDirty = async () => { try { await Promise.resolve(onSaveUrlConfig()); urlListDirtyRef.current = false; } catch { /* parent 已 toast */ } }; const lock = busy || urlReloading; const presetLabel = PRESETS.find((p) => p.id === selectedPreset); const checklist: { id: string; zh: string; en: string }[] = [ { id: "cable", zh: "HDMI 线材插紧、方向正确(如需转接头确认规格)", en: "HDMI cable seated; adapter specs OK" }, { id: "edid", zh: "采集卡与板端分辨率/刷新率一致(见上方预设)", en: "Capture + SoC timing match preset" }, { id: "usb", zh: "USB 采集盒接 USB3 口,避免过长无屏蔽线", en: "USB3 capture; avoid bad cables" }, { id: "thermal", zh: "长时间推流注意散热,过热会掉帧黑屏", en: "Thermal headroom for long runs" }, ]; return (

{t("TikTok 无人直播(单路)", "TikTok unmanned (single lane)")}

{t( "布局对齐 YouTube 单频道:直播开关 + 源站 + URL_config;无 stream key,输出走 HDMI 硬件链路。", "Same layout as YouTube single: controls + URLs; no stream key — HDMI hardware path.", )}

{t("直播开关", "Live control")}

{( [ ["start", t("开始直播", "Start"), "bg-emerald-500/20 text-emerald-200 ring-emerald-500/30"], ["stop", t("停止直播", "Stop"), "bg-rose-500/15 text-rose-200 ring-rose-500/25"], ["restart", t("重新开始", "Restart"), "bg-amber-500/15 text-amber-200 ring-amber-500/25"], ] as const ).map(([a, label, cls]) => ( ))}

{t("当前 HDMI 预设(备忘)", "HDMI preset (memo)")}:{" "} {presetLabel ? t(presetLabel.labelZh, presetLabel.labelEn) : selectedPreset}

{t("HDMI 硬件与时序", "HDMI timing")}

{t("点选一路作为现场备忘;与板端 ffmpeg/OBS 采集参数对齐。", "Pick a preset as field memo; match capture settings.")}

{PRESETS.map((p) => { const open = expanded === p.id; const sel = selectedPreset === p.id; return ( ); })}

{t("上线前检查清单(本地保存)", "Pre-flight checklist (local)")}

    {checklist.map((c) => (
  • persistChecks({ ...checks, [c.id]: e.target.checked })} /> {t(c.zh, c.en)}
  • ))}