mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-06-17 15:05:39 +02:00
render manifest ffmpeg progress like standard download bars
This commit is contained in:
@@ -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 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 {
|
||||
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 {
|
||||
targetMS := state.currentMS
|
||||
if targetMS > state.totalMS {
|
||||
targetMS = state.totalMS
|
||||
}
|
||||
if targetMS > shownMS {
|
||||
bar.IncrBy(int(targetMS - shownMS))
|
||||
shownMS = targetMS
|
||||
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
|
||||
}()
|
||||
@@ -469,6 +493,8 @@ func buildFFmpegStreamArgs(sourceURL, outputPath string, includeVideo bool) []st
|
||||
type scanState struct {
|
||||
totalMS int64
|
||||
currentMS int64
|
||||
bitrateBPS int64
|
||||
currentBytes int64
|
||||
scanErr error
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -74,7 +74,6 @@ func TestDeezerBlowfishKeyDerivation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func TestFileDeezerEncrypted(t *testing.T) {
|
||||
trackID := "3135556"
|
||||
plain := make([]byte, deezerBFChunkSize+777)
|
||||
@@ -221,6 +220,36 @@ func TestParseFFmpegDurationLine(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFFmpegDurationBitrateBPS(t *testing.T) {
|
||||
bps, ok := parseFFmpegDurationBitrateBPS(" Duration: 00:04:52.57, start: 0.000000, bitrate: 975 kb/s")
|
||||
if !ok {
|
||||
t.Fatalf("expected bitrate parse to succeed")
|
||||
}
|
||||
if want := int64(975000); bps != want {
|
||||
t.Fatalf("unexpected bitrate: got=%d want=%d", bps, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFFmpegProgressBitrateBPS(t *testing.T) {
|
||||
bps, ok := parseFFmpegProgressBitrateBPS("bitrate=1706.8kbits/s")
|
||||
if !ok {
|
||||
t.Fatalf("expected progress bitrate parse to succeed")
|
||||
}
|
||||
if want := int64(1706800); bps != want {
|
||||
t.Fatalf("unexpected bitrate: got=%d want=%d", bps, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFFmpegTotalSize(t *testing.T) {
|
||||
size, ok := parseFFmpegTotalSize("total_size=1234567")
|
||||
if !ok {
|
||||
t.Fatalf("expected total_size parse to succeed")
|
||||
}
|
||||
if want := int64(1234567); size != want {
|
||||
t.Fatalf("unexpected total_size: got=%d want=%d", size, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFFmpegOutTime(t *testing.T) {
|
||||
currentMS, ok := parseFFmpegOutTime("out_time=00:01:02.340000")
|
||||
if !ok {
|
||||
@@ -240,6 +269,20 @@ func TestParseClockDurationMSInvalid(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEstimateTotalBytesFromBitrate(t *testing.T) {
|
||||
total := estimateTotalBytesFromBitrate(10000, 1600000)
|
||||
if want := int64(2000000); total != want {
|
||||
t.Fatalf("unexpected estimate from bitrate: got=%d want=%d", total, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEstimateTotalBytesFromProgress(t *testing.T) {
|
||||
total := estimateTotalBytesFromProgress(10000, 2500, 500000)
|
||||
if want := int64(2000000); total != want {
|
||||
t.Fatalf("unexpected estimate from progress: got=%d want=%d", total, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFFmpegStreamArgsAudioOnly(t *testing.T) {
|
||||
args := buildFFmpegStreamArgs("https://example.com/master.m3u8", "/tmp/out.m4a", false)
|
||||
if !containsArgPair(args, "-map", "0:a:0") {
|
||||
|
||||
Reference in New Issue
Block a user