package video import ( "bytes" "fmt" "io" "log" "net/http" "os/exec" "strings" "syscall" "time" ) func Stream(w http.ResponseWriter, r *http.Request, absPath, ffmpegPath string) error { if ffmpegPath == "" { ffmpegPath = defaultFFmpegPath } cmd := exec.Command( ffmpegPath, "-loglevel", "error", "-i", absPath, "-c:v", "copy", "-movflags", "frag_keyframe+empty_moov", "-f", "mp4", "pipe:1", ) var stderr bytes.Buffer cmd.Stderr = &stderr stdout, err := cmd.StdoutPipe() if err != nil { return fmt.Errorf("open ffmpeg stdout: %w", err) } if err := cmd.Start(); err != nil { return fmt.Errorf("start ffmpeg: %w%s", err, stderrSuffix(stderr.String())) } processDone := make(chan struct{}) go func() { select { case <-r.Context().Done(): if cmd.Process == nil { return } _ = cmd.Process.Signal(syscall.SIGTERM) timer := time.NewTimer(250 * time.Millisecond) defer timer.Stop() select { case <-processDone: case <-timer.C: _ = cmd.Process.Kill() } case <-processDone: } }() w.Header().Set("Cache-Control", "no-store") w.Header().Set("Content-Type", "video/mp4") w.Header().Set("X-Content-Type-Options", "nosniff") w.WriteHeader(http.StatusOK) if flusher, ok := w.(http.Flusher); ok { flusher.Flush() } _, copyErr := io.Copy(w, stdout) waitErr := cmd.Wait() close(processDone) if r.Context().Err() != nil { return nil } if copyErr != nil { log.Printf("ffmpeg remux copy failed for %s: %v", absPath, copyErr) return nil } if waitErr != nil { log.Printf("ffmpeg remux failed for %s: %v%s", absPath, waitErr, stderrSuffix(stderr.String())) } return nil } func stderrSuffix(stderr string) string { trimmed := strings.TrimSpace(stderr) if trimmed == "" { return "" } return ": " + trimmed }