- 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>
116 lines
2.2 KiB
Go
116 lines
2.2 KiB
Go
package process
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os/exec"
|
|
"sync"
|
|
"time"
|
|
|
|
"tuner/models"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
// NewFFmpegManager creates a new FFmpeg process manager.
|
|
func NewFFmpegManager() *FFmpegManager {
|
|
return &FFmpegManager{}
|
|
}
|
|
|
|
// Start spawns an FFmpeg process to pull from streamURL and push RTMP to MediaMTX.
|
|
// It kills any existing FFmpeg process first.
|
|
func (f *FFmpegManager) Start(streamURL string) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
|
|
// Kill existing process if running
|
|
f.stopLocked()
|
|
|
|
f.cmd = exec.Command("ffmpeg",
|
|
"-re",
|
|
"-i", streamURL,
|
|
"-c", "copy",
|
|
"-f", "flv",
|
|
"rtmp://localhost:1935/live/stream",
|
|
)
|
|
f.streamURL = streamURL
|
|
f.lastError = ""
|
|
|
|
if err := f.cmd.Start(); err != nil {
|
|
f.lastError = err.Error()
|
|
return fmt.Errorf("start ffmpeg: %w", err)
|
|
}
|
|
|
|
f.startTime = time.Now()
|
|
log.Printf("[ffmpeg] started with pid %d, source: %s", f.cmd.Process.Pid, streamURL)
|
|
|
|
// Monitor in background
|
|
go f.monitor()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (f *FFmpegManager) monitor() {
|
|
err := f.cmd.Wait()
|
|
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
|
|
if err != nil {
|
|
f.lastError = err.Error()
|
|
log.Printf("[ffmpeg] exited: %v", err)
|
|
} else {
|
|
log.Println("[ffmpeg] exited (status 0)")
|
|
}
|
|
|
|
f.cmd = nil
|
|
}
|
|
|
|
// Stop kills the FFmpeg process.
|
|
func (f *FFmpegManager) Stop() error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return f.stopLocked()
|
|
}
|
|
|
|
func (f *FFmpegManager) stopLocked() error {
|
|
if f.cmd == nil || f.cmd.Process == nil {
|
|
return nil
|
|
}
|
|
|
|
if err := f.cmd.Process.Kill(); err != nil {
|
|
return fmt.Errorf("kill ffmpeg: %w", err)
|
|
}
|
|
|
|
// Wait for the process to fully exit to avoid zombies
|
|
_ = f.cmd.Wait()
|
|
f.cmd = nil
|
|
log.Println("[ffmpeg] killed")
|
|
return nil
|
|
}
|
|
|
|
// Status returns the current FFmpeg process status.
|
|
func (f *FFmpegManager) Status() models.ProcessInfo {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
|
|
info := models.ProcessInfo{
|
|
Error: f.lastError,
|
|
}
|
|
|
|
if f.cmd != nil && f.cmd.Process != nil {
|
|
info.Running = true
|
|
info.PID = f.cmd.Process.Pid
|
|
info.Uptime = time.Since(f.startTime).Truncate(time.Second).String()
|
|
}
|
|
|
|
return info
|
|
}
|