package transcode import ( "bufio" "encoding/json" "fmt" "os" "os/exec" "regexp" "strconv" "strings" ) // Segment is a time interval [Start, End) in seconds. type Segment struct { Start float64 `json:"start"` End float64 `json:"end"` } type wsMsg struct { Type string `json:"type"` Segments []Segment `json:"segments,omitempty"` Duration float64 `json:"duration,omitempty"` Percent float64 `json:"percent,omitempty"` Message string `json:"message,omitempty"` } func send(msg *wsMsg, broadcast func([]byte)) { data, _ := json.Marshal(&msg) broadcast(data) } func ExportSegments( inputPath, outputPath string, segments []Segment, broadcast func([]byte), ) error { totalDuration := 0.0 for _, seg := range segments { totalDuration += seg.End - seg.Start } // Build a filter_complex that trims each segment and resets timestamps, // then concatenates them. This avoids non-monotonic DTS issues that occur // when stream-copying segments with their original timestamps. var filterParts []string var concatInputs string for i, seg := range segments { filterParts = append(filterParts, fmt.Sprintf("[0:v]trim=start=%.6f:end=%.6f,setpts=PTS-STARTPTS[v%d]", seg.Start, seg.End, i), fmt.Sprintf("[0:a]atrim=start=%.6f:end=%.6f,asetpts=PTS-STARTPTS[a%d]", seg.Start, seg.End, i), ) concatInputs += fmt.Sprintf("[v%d][a%d]", i, i) } filterParts = append(filterParts, fmt.Sprintf("%sconcat=n=%d:v=1:a=1[outv][outa]", concatInputs, len(segments)), ) filterComplex := strings.Join(filterParts, ";") cmd := exec.Command("ffmpeg", "-i", inputPath, "-filter_complex", filterComplex, "-map", "[outv]", "-map", "[outa]", // around 80% compression "-c:v", "libx264", "-crf", "28", "-preset", "veryfast", "-c:a", "aac", "-b:a", "128k", "-progress", "pipe:1", "-y", outputPath, ) stdout, err := cmd.StdoutPipe() if err != nil { return err } cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { return fmt.Errorf("ffmpeg start: %w", err) } timeRe := regexp.MustCompile(`out_time_ms=(\d+)`) scanner := bufio.NewScanner(stdout) for scanner.Scan() { if m := timeRe.FindStringSubmatch(scanner.Text()); m != nil { ms, err := strconv.ParseFloat(m[1], 64) if err != nil || ms < 0 { continue } pct := (ms / 1e6) / totalDuration * 100 if pct > 100 { pct = 100 } send(&wsMsg{Type: "progress", Percent: pct}, broadcast) } } if err := cmd.Wait(); err != nil { return fmt.Errorf("ffmpeg: %w", err) } return nil }