render manifest ffmpeg progress like standard download bars

This commit is contained in:
2026-04-22 00:40:11 +02:00
parent 50ca5f564b
commit c1e89f5876
2 changed files with 217 additions and 36 deletions

View File

@@ -343,8 +343,9 @@ func (d *Downloader) streamManifestWithFFmpeg(ctx context.Context, sourceURL, ou
decor.Name(desc+" ", decor.WC{W: 56, C: decor.DSyncWidth | decor.DindentRight}),
),
mpb.AppendDecorators(
decor.CurrentKibiByte("% .1f", decor.WCSyncWidthR),
decor.Name(" | ", decor.WCSyncWidth),
decor.Elapsed(decor.ET_STYLE_GO, decor.WCSyncWidthR),
decor.Name(" | probing stream", decor.WCSyncWidth),
),
mpb.BarRemoveOnComplete(),
)
@@ -364,18 +365,21 @@ func (d *Downloader) streamManifestWithFFmpeg(ctx context.Context, sourceURL, ou
scanDone := make(chan scanState, 1)
go func() {
state := scanState{}
shownMS := int64(0)
newFFmpegBar := func(totalMS int64) *mpb.Bar {
shownBytes := int64(0)
barTotalBytes := int64(0)
newEstimatedBar := func(totalBytes int64) *mpb.Bar {
return d.progress.AddBar(
totalMS,
totalBytes,
mpb.PrependDecorators(
decor.Name(desc+" ", decor.WC{W: 56, C: decor.DSyncWidth | decor.DindentRight}),
decor.Percentage(decor.WCSyncWidthR),
),
mpb.AppendDecorators(
decor.CountersKibiByte("% .1f / % .1f", decor.WCSyncWidthR),
decor.Name(" | ", decor.WCSyncWidth),
decor.Elapsed(decor.ET_STYLE_GO, decor.WCSyncWidthR),
decor.Name(" | ffmpeg stream", decor.WCSyncWidth),
decor.AverageSpeed(decor.SizeB1024(0), "% .1f", decor.WCSyncWidthR),
decor.Name(" | ETA ", decor.WCSyncWidth),
decor.AverageETA(decor.ET_STYLE_GO, decor.WCSyncWidthR),
),
mpb.BarRemoveOnComplete(),
)
@@ -389,45 +393,65 @@ func (d *Downloader) streamManifestWithFFmpeg(ctx context.Context, sourceURL, ou
ffOut.WriteString(line)
ffOut.WriteByte('\n')
}
if state.totalMS == 0 {
if totalMS, ok := parseFFmpegDurationLine(line); ok && totalMS > 0 {
state.totalMS = totalMS
spinner.SetTotal(-1, true)
bar = newFFmpegBar(state.totalMS)
if state.currentMS > state.totalMS {
state.currentMS = state.totalMS
}
if state.currentMS > shownMS {
bar.IncrBy(int(state.currentMS - shownMS))
shownMS = state.currentMS
}
}
if totalMS, ok := parseFFmpegDurationLine(line); ok && totalMS > 0 {
state.totalMS = totalMS
}
if bitrateBPS, ok := parseFFmpegDurationBitrateBPS(line); ok && bitrateBPS > 0 {
state.bitrateBPS = bitrateBPS
}
if bitrateBPS, ok := parseFFmpegProgressBitrateBPS(line); ok && bitrateBPS > 0 {
state.bitrateBPS = bitrateBPS
}
if totalSize, ok := parseFFmpegTotalSize(line); ok && totalSize > state.currentBytes {
state.currentBytes = totalSize
}
if currentMS, ok := parseFFmpegOutTime(line); ok {
if currentMS > state.currentMS {
state.currentMS = currentMS
}
if bar != nil {
targetMS := state.currentMS
if targetMS > state.totalMS {
targetMS = state.totalMS
}
if targetMS > shownMS {
bar.IncrBy(int(targetMS - shownMS))
shownMS = targetMS
}
}
if bar == nil {
estimatedTotal := int64(0)
if state.totalMS > 0 && state.bitrateBPS > 0 {
estimatedTotal = estimateTotalBytesFromBitrate(state.totalMS, state.bitrateBPS)
}
if estimatedTotal == 0 {
estimatedTotal = estimateTotalBytesFromProgress(state.totalMS, state.currentMS, state.currentBytes)
}
if estimatedTotal > 0 {
if estimatedTotal < state.currentBytes {
estimatedTotal = state.currentBytes
}
barTotalBytes = estimatedTotal
spinner.SetTotal(-1, true)
bar = newEstimatedBar(barTotalBytes)
}
}
if state.currentBytes > shownBytes {
delta := state.currentBytes - shownBytes
if bar != nil {
bar.IncrBy(int(delta))
if state.currentBytes > barTotalBytes {
barTotalBytes = state.currentBytes
bar.SetTotal(barTotalBytes, false)
}
} else {
spinner.IncrBy(int(delta))
}
shownBytes = state.currentBytes
}
}
if bar != nil {
if shownMS < state.totalMS {
bar.IncrBy(int(state.totalMS - shownMS))
if shownBytes < barTotalBytes {
bar.IncrBy(int(barTotalBytes - shownBytes))
}
bar.SetTotal(state.totalMS, true)
bar.SetTotal(barTotalBytes, true)
} else {
spinner.SetTotal(-1, true)
}
state.currentMS = shownMS
state.currentBytes = shownBytes
state.scanErr = scanner.Err()
scanDone <- state
}()
@@ -467,9 +491,11 @@ func buildFFmpegStreamArgs(sourceURL, outputPath string, includeVideo bool) []st
}
type scanState struct {
totalMS int64
currentMS int64
scanErr error
totalMS int64
currentMS int64
bitrateBPS int64
currentBytes int64
scanErr error
}
func isManifestResponse(contentType string, peek []byte) bool {
@@ -513,6 +539,118 @@ func parseFFmpegOutTime(line string) (int64, bool) {
return parseClockDurationMS(raw)
}
func parseFFmpegTotalSize(line string) (int64, bool) {
if !strings.HasPrefix(line, "total_size=") {
return 0, false
}
raw := strings.TrimSpace(strings.TrimPrefix(line, "total_size="))
if raw == "" || strings.EqualFold(raw, "N/A") {
return 0, false
}
v, err := strconv.ParseInt(raw, 10, 64)
if err != nil || v < 0 {
return 0, false
}
return v, true
}
func parseFFmpegDurationBitrateBPS(line string) (int64, bool) {
idx := strings.Index(strings.ToLower(line), "bitrate:")
if idx < 0 {
return 0, false
}
raw := strings.TrimSpace(line[idx+len("bitrate:"):])
if raw == "" {
return 0, false
}
return parseFFmpegBitrateBPS(raw)
}
func parseFFmpegProgressBitrateBPS(line string) (int64, bool) {
if !strings.HasPrefix(line, "bitrate=") {
return 0, false
}
raw := strings.TrimSpace(strings.TrimPrefix(line, "bitrate="))
if raw == "" || strings.EqualFold(raw, "N/A") {
return 0, false
}
return parseFFmpegBitrateBPS(raw)
}
func parseFFmpegBitrateBPS(raw string) (int64, bool) {
raw = strings.TrimSpace(raw)
if raw == "" || strings.EqualFold(raw, "N/A") {
return 0, false
}
parts := strings.Fields(strings.ToLower(raw))
if len(parts) == 0 {
return 0, false
}
numStr := parts[0]
unit := ""
if len(parts) > 1 {
unit = parts[1]
} else {
i := 0
for i < len(numStr) {
c := numStr[i]
if (c >= '0' && c <= '9') || c == '.' {
i++
continue
}
break
}
if i < len(numStr) {
unit = numStr[i:]
numStr = numStr[:i]
}
}
num, err := strconv.ParseFloat(strings.TrimSpace(numStr), 64)
if err != nil || num <= 0 {
return 0, false
}
unit = strings.TrimSpace(strings.Trim(unit, ","))
mult := float64(1)
switch unit {
case "kb/s", "kbits/s", "kbit/s", "kbps":
mult = 1000
case "mb/s", "mbits/s", "mbit/s", "mbps":
mult = 1000 * 1000
case "gb/s", "gbits/s", "gbit/s", "gbps":
mult = 1000 * 1000 * 1000
case "b/s", "bit/s", "bits/s", "":
mult = 1
default:
return 0, false
}
bps := int64(num * mult)
if bps <= 0 {
return 0, false
}
return bps, true
}
func estimateTotalBytesFromBitrate(totalMS, bitrateBPS int64) int64 {
if totalMS <= 0 || bitrateBPS <= 0 {
return 0
}
return (totalMS * bitrateBPS) / 8000
}
func estimateTotalBytesFromProgress(totalMS, currentMS, currentBytes int64) int64 {
if totalMS <= 0 || currentMS <= 0 || currentBytes <= 0 {
return 0
}
if currentMS > totalMS {
currentMS = totalMS
}
est := (currentBytes * totalMS) / currentMS
if est < currentBytes {
est = currentBytes
}
return est
}
func parseClockDurationMS(raw string) (int64, bool) {
parts := strings.Split(strings.TrimSpace(raw), ":")
if len(parts) != 3 {