Implement Tuner v1 — Go backend, React frontend, Docker setup

- Go backend: REST API, M3U parser, FFmpeg/MediaMTX process manager
- React/Vite frontend: HLS player, admin panel, channel browser (dark theme)
- MediaMTX config for RTMP ingest + HLS output
- Multi-stage Dockerfile (Go + Bun + Alpine runtime)
- docker-compose.yml for single-container deployment
- Sample M3U playlist with test streams

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-07 09:47:32 -08:00
parent 2e66b8c73d
commit b8bfcefee8
33 changed files with 2257 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
import { useEffect, useRef } from "react";
import Hls from "hls.js";
export default function Player() {
const videoRef = useRef<HTMLVideoElement>(null);
const hlsRef = useRef<Hls | null>(null);
useEffect(() => {
const video = videoRef.current;
if (!video) return;
const src = `http://${window.location.hostname}:8888/live/stream/`;
if (Hls.isSupported()) {
const hls = new Hls({
enableWorker: true,
lowLatencyMode: true,
});
hlsRef.current = hls;
hls.loadSource(src);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
video.play().catch(() => {});
});
hls.on(Hls.Events.ERROR, (_event, data) => {
if (data.fatal) {
if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
setTimeout(() => hls.loadSource(src), 5000);
} else {
hls.destroy();
}
}
});
} else if (video.canPlayType("application/vnd.apple.mpegurl")) {
video.src = src;
video.addEventListener("loadedmetadata", () => {
video.play().catch(() => {});
});
}
return () => {
if (hlsRef.current) {
hlsRef.current.destroy();
hlsRef.current = null;
}
};
}, []);
return (
<div className="player-container">
<video ref={videoRef} className="player-video" controls playsInline />
<div className="player-offline">Stream Offline</div>
</div>
);
}