117 lines
2.8 KiB
Go
117 lines
2.8 KiB
Go
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
|
|
}
|