package ffmpeg import ( "regexp" "strconv" "strings" ) // Progress holds real-time encoding progress from ffmpeg stderr. type Progress struct { Frame int64 `json:"frame"` FPS float64 `json:"fps"` Q float64 `json:"q"` Size string `json:"size"` Time string `json:"time"` Bitrate string `json:"bitrate"` Speed string `json:"speed"` Eta string `json:"eta"` Percent float64 `json:"percent"` } // kvPairRe matches a single key=value token from an ffmpeg progress line. // Keys are alphanumeric (e.g. frame, dup, Lsize). Values are the contiguous // non-whitespace characters after the '=' — this handles N/A, kbits/s, times, // and numeric values equally. var kvPairRe = regexp.MustCompile(`(\w+)=\s*(\S+)`) // parseProgressLine parses an ffmpeg stderr progress line into a Progress struct. // Instead of relying on a fixed field order, it extracts every key=value pair // and picks the ones we care about. This tolerates extra fields (dup, drop, // Lsize vs size, etc.) that appear between standard ffmpeg releases. func parseProgressLine(line string) (Progress, bool) { pairs := kvPairRe.FindAllStringSubmatch(line, -1) if len(pairs) == 0 { return Progress{}, false } kv := make(map[string]string, len(pairs)) for _, m := range pairs { kv[m[1]] = m[2] } // Frame is required — without it we don't have a real progress line. frameStr, ok := kv["frame"] if !ok { return Progress{}, false } frame, _ := strconv.ParseInt(frameStr, 10, 64) fps, _ := strconv.ParseFloat(kv["fps"], 64) q, _ := strconv.ParseFloat(kv["q"], 64) // ffmpeg emits either "size" or "Lsize" depending on context. size := kv["size"] if size == "" { size = kv["Lsize"] } if size == "N/A" { size = "" } bitrate := kv["bitrate"] if bitrate == "N/A" { bitrate = "" } return Progress{ Frame: frame, FPS: fps, Q: q, Size: size, Time: kv["time"], Bitrate: bitrate, Speed: kv["speed"], }, true } // parseSpeedMultiplier converts "1.5x" → 1.5. func parseSpeedMultiplier(s string) float64 { s = strings.TrimSuffix(s, "x") v, _ := strconv.ParseFloat(s, 64) return v } // formatSeconds converts seconds to "MM:SS" or "HH:MM:SS". func formatSeconds(secs float64) string { if secs < 0 { secs = 0 } h := int(secs) / 3600 m := (int(secs) % 3600) / 60 s := int(secs) % 60 if h > 0 { return strconv.Itoa(h) + ":" + pad2(m) + ":" + pad2(s) } return pad2(m) + ":" + pad2(s) } func pad2(n int) string { if n < 10 { return "0" + strconv.Itoa(n) } return strconv.Itoa(n) } // parseTimeSeconds converts "HH:MM:SS.mm" to seconds. func parseTimeSeconds(t string) float64 { parts := strings.Split(t, ":") if len(parts) != 3 { return 0 } h, _ := strconv.ParseFloat(parts[0], 64) m, _ := strconv.ParseFloat(parts[1], 64) s, _ := strconv.ParseFloat(parts[2], 64) return h*3600 + m*60 + s }