/** * @license * SPDX-License-Identifier: Apache-2.0 */ import React, { useState, useRef, useEffect, useCallback } from 'react'; import { MonitorPlay, SquareSquare, StopCircle, Activity, Settings2, Video, Radio, Eye, EyeOff, Copy, Check } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; import flvjs from 'flv.js'; import { v4 as uuidv4 } from 'uuid'; export default function App() { const [isStreaming, setIsStreaming] = useState(false); const [streamConfig, setStreamConfig] = useState<{ rtmpUrl: string, streamKey: string, isNgrok?: boolean } | null>(null); const [showKey, setShowKey] = useState(false); const [copied, setCopied] = useState(false); const [isStreamActiveBackend, setIsStreamActiveBackend] = useState(false); const [stats, setStats] = useState({ fps: 0, kbps: 0, droppedFrames: 0, codec: 'H.264/AAC', resolution: 'Awaiting Signal', jitter: 0 }); const videoRef = useRef(null); const flvPlayerRef = useRef(null); const statsIntervalRef = useRef(null); useEffect(() => { // Generate stream key and get RTMP URL on mount fetch('/api/config') .then(res => res.json()) .then(data => { setStreamConfig({ rtmpUrl: data.rtmpUrl, streamKey: uuidv4().substring(0, 13).replace(/-/g, ''), // e.g. a1b2c3d4e5f6 isNgrok: data.isNgrok }); }); }, []); useEffect(() => { if (!streamConfig?.streamKey) return; let mounted = true; const checkBackendActive = async () => { try { const res = await fetch(`/api/streams/live/${streamConfig.streamKey}`); if (!res.ok) return; const data = await res.json(); if (mounted) { setIsStreamActiveBackend(data.active); } } catch (e) { // ignore fetch errors } }; checkBackendActive(); const interval = setInterval(checkBackendActive, 2000); return () => { mounted = false; clearInterval(interval); }; }, [streamConfig?.streamKey]); const copyToClipboard = () => { if (streamConfig) { navigator.clipboard.writeText(streamConfig.streamKey); setCopied(true); setTimeout(() => setCopied(false), 2000); } }; const startPlayer = () => { if (!flvjs.isSupported() || !videoRef.current || !streamConfig) return; if (flvPlayerRef.current) { flvPlayerRef.current.destroy(); } const protocol = window.location.protocol === 'https:' ? 'https:' : 'http:'; const host = window.location.host; const fetchUrl = `${protocol}//${host}/live/${streamConfig.streamKey}.flv`; const flvPlayer = flvjs.createPlayer({ type: 'flv', url: fetchUrl, isLive: true, hasAudio: true, hasVideo: true, }); flvPlayer.attachMediaElement(videoRef.current); flvPlayer.load(); // We try to play, but it might fail if there's no stream yet. // The user should start the player AFTER starting the stream in OBS, or we can handle errors. try { const playPromise = flvPlayer.play() as Promise | undefined; if (playPromise !== undefined) { playPromise.catch(() => { console.log("Playback failed. Awaiting stream..."); }); } } catch (e) { console.log("Playback failed. Awaiting stream...", e); } flvPlayer.on(flvjs.Events.ERROR, (errType, errDetail) => { console.error("FLV Player Error:", errType, errDetail); if (errType === flvjs.ErrorTypes.NETWORK_ERROR) { // Stream not live yet, or disconnected setIsStreaming(false); } }); flvPlayer.on(flvjs.Events.STATISTICS_INFO, (statInfo) => { setIsStreaming(true); // if we get stats, we are receiving video! setStats(prev => ({ ...prev, fps: Math.round(statInfo.decodedFrames / Math.max(1, (videoRef.current?.currentTime || 1)) ), // Rough estimate or just rely on NMS kbps: Math.round(statInfo.speed * 8), // speed is usually bytes per second in some versions, or kbps droppedFrames: statInfo.droppedFrames || 0, resolution: `${videoRef.current?.videoWidth || 0}x${videoRef.current?.videoHeight || 0}` })); }); flvPlayerRef.current = flvPlayer; setIsStreaming(true); // Simulated Stats polling (since flv.js stats might be limited) statsIntervalRef.current = setInterval(() => { if (videoRef.current && videoRef.current.videoWidth > 0) { setStats(prev => ({ ...prev, resolution: `${videoRef.current?.videoWidth}x${videoRef.current?.videoHeight}` })); } }, 1000); }; const stopPlayer = () => { if (flvPlayerRef.current) { flvPlayerRef.current.destroy(); flvPlayerRef.current = null; } if (statsIntervalRef.current) { clearInterval(statsIntervalRef.current); } if (videoRef.current) { videoRef.current.src = ""; // Clear video } setIsStreaming(false); setStats({ fps: 0, kbps: 0, droppedFrames: 0, codec: 'H.264/AAC', resolution: 'Awaiting Signal', jitter: 0 }); }; useEffect(() => { return stopPlayer; }, []); return (
{/* Header Section */}

RTMP COMMAND CENTER v2.4.0

Node Media Server

OPERATIONAL

Player State

{isStreaming ? 'CONNECTED' : 'DISCONNECTED'}

{/* Sidebar Left: Config */}

Ingest Configuration

{streamConfig?.rtmpUrl || 'Loading...'}
{showKey ? streamConfig?.streamKey : "•••• •••• •••• ••••"}

Source Details

Incoming Resp {stats.resolution}
Video Codec H.264 / AVC
Audio Codec AAC 48kHz

System Logs

[SYSTEM] RTMP Server bound to :1935

[SYSTEM] HTTP-FLV muxing active

{streamConfig?.isNgrok ? (

Bore.pub TCP Tunnel is ACTIVE! Ready for Streamlabs connection.

) : ( <>

Establishing proxy tunnel for OBS / Streamlabs compatibility...

)}
{!isStreaming ? ( {isStreamActiveBackend ? ( <> CONNECT PLAYER ) : ( <>
AWAITING STREAM... )}
) : ( DISCONNECT PLAYER )}
{/* Main Video View */}
{/* Placeholder if not streaming */} {!isStreaming && (

AWAITING RTMP SIGNAL

Start streaming from OBS to the Server URL, then click Connect Player.

)}
{/* Telemetry Dashboard */}

Network

{stats.kbps.toLocaleString()} kbps

Client Frame Rate

{stats.fps > 0 ? stats.fps : '-'} FPS

Client Dropped

0 ? 'text-rose-400' : 'text-zinc-100'}`}>{stats.droppedFrames} Frames

Demuxer

{stats.codec.toUpperCase()}
); }