105 lines
2.5 KiB
Go
105 lines
2.5 KiB
Go
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
|
|
}
|