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

69
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,69 @@
import { useState, useEffect, useCallback } from "react";
import type { StreamStatus, Channel } from "./types";
import * as api from "./api/client";
import Player from "./components/Player";
import StatusBar from "./components/StatusBar";
import AdminPanel from "./components/AdminPanel";
import ChannelList from "./components/ChannelList";
import "./App.css";
export default function App() {
const [status, setStatus] = useState<StreamStatus | null>(null);
const [channels, setChannels] = useState<Channel[]>([]);
const fetchStatus = useCallback(async () => {
try {
const s = await api.getStatus();
setStatus(s);
} catch {
// backend unreachable
}
}, []);
const fetchChannels = useCallback(async () => {
try {
const ch = await api.getChannels();
setChannels(ch);
} catch {
// backend unreachable
}
}, []);
useEffect(() => {
fetchStatus();
fetchChannels();
const interval = setInterval(fetchStatus, 5000);
return () => clearInterval(interval);
}, [fetchStatus, fetchChannels]);
const handleSourceChanged = () => {
fetchStatus();
};
const handlePlaylistReloaded = () => {
fetchChannels();
};
const handleChannelRefresh = () => {
fetchStatus();
};
return (
<div className="app">
<Player />
<StatusBar status={status} />
<div className="controls">
<AdminPanel
status={status}
onSourceChanged={handleSourceChanged}
onPlaylistReloaded={handlePlaylistReloaded}
/>
<ChannelList
channels={channels}
activeChannel={status?.channelName ?? ""}
onRefresh={handleChannelRefresh}
/>
</div>
</div>
);
}