Tuner v1: HLS pipeline with playback/ingest modes

FFmpeg -> MediaMTX -> HLS relay with runtime-configurable modes:
- PLAYBACK_MODE: standard (mpegts), fmp4 (HEVC), llhls
- INGEST_MODE: rtmp, rtsp
- INGEST_AUDIO_MODE: aac, copy
- Hourly playlist auto-refresh from upstream URL
- Fullscreen video UI with collapsible sidebar, upstream-down banner
- Same-origin HLS proxy for HTTPS deployments

Tagged as the last HLS-based version before the mpegts.js (v2) rework.
This commit is contained in:
2026-06-12 19:17:15 -07:00
parent a41c3ee56c
commit 88a189de34
20 changed files with 814 additions and 123 deletions

View File

@@ -15,17 +15,25 @@ import (
// FFmpegManager manages an FFmpeg child process for IPTV relay.
type FFmpegManager struct {
mu sync.Mutex
cmd *exec.Cmd
startTime time.Time
lastError string
streamURL string
health models.StreamHealth
mu sync.Mutex
cmd *exec.Cmd
ingestMode string
audioMode string
startTime time.Time
lastError string
streamURL string
health models.StreamHealth
}
// NewFFmpegManager creates a new FFmpeg process manager.
func NewFFmpegManager() *FFmpegManager {
return &FFmpegManager{}
func NewFFmpegManager(ingestMode, audioMode string) *FFmpegManager {
if ingestMode != "rtsp" {
ingestMode = "rtmp"
}
if audioMode != "copy" {
audioMode = "aac"
}
return &FFmpegManager{ingestMode: ingestMode, audioMode: audioMode}
}
// Start spawns an FFmpeg process to pull from streamURL and push to MediaMTX.
@@ -37,17 +45,39 @@ func (f *FFmpegManager) Start(streamURL string) error {
// Kill existing process if running
f.stopLocked()
f.cmd = exec.Command("ffmpeg",
args := []string{
"-loglevel", "warning",
"-progress", "pipe:1",
"-reconnect", "1",
"-reconnect_streamed", "1",
"-reconnect_delay_max", "5",
"-i", streamURL,
"-c", "copy",
"-f", "flv",
"rtmp://localhost:1935/live/stream",
)
}
if f.ingestMode == "rtsp" {
if f.audioMode == "copy" {
args = append(args, "-c", "copy")
} else {
args = append(args,
"-c:v", "copy",
"-c:a", "aac",
"-ar", "48000",
"-b:a", "160k",
)
}
args = append(args,
"-f", "rtsp",
"-rtsp_transport", "tcp",
"rtsp://localhost:8554/live/stream",
)
} else {
args = append(args,
"-c", "copy",
"-f", "flv",
"rtmp://localhost:1935/live/stream",
)
}
f.cmd = exec.Command("ffmpeg", args...)
f.streamURL = streamURL
f.lastError = ""
f.health = models.StreamHealth{}
@@ -68,7 +98,7 @@ func (f *FFmpegManager) Start(streamURL string) error {
}
f.startTime = time.Now()
log.Printf("[ffmpeg] started with pid %d, source: %s", f.cmd.Process.Pid, streamURL)
log.Printf("[ffmpeg] started with pid %d, ingest=%s, audio=%s, source: %s", f.cmd.Process.Pid, f.ingestMode, f.audioMode, streamURL)
// Pass cmd reference so goroutines can detect process replacement
cmd := f.cmd