mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-06-17 15:05:39 +02:00
improve ffmpeg manifest progress rendering
This commit is contained in:
@@ -14,7 +14,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/vbauerster/mpb/v8"
|
||||
"github.com/vbauerster/mpb/v8/decor"
|
||||
@@ -320,41 +319,6 @@ func shortenName(name string, max int) string {
|
||||
}
|
||||
|
||||
func (d *Downloader) streamManifestWithFFmpeg(ctx context.Context, sourceURL, outputPath string, includeVideo bool) error {
|
||||
stopSpinner := func() {}
|
||||
if d.ProgressEnabled() {
|
||||
d.barStarted.Store(1)
|
||||
desc := shortenName(filepath.Base(outputPath), 54)
|
||||
spin := d.progress.AddSpinner(
|
||||
0,
|
||||
mpb.PrependDecorators(
|
||||
decor.Name(desc+" ", decor.WC{W: 56, C: decor.DSyncWidth | decor.DindentRight}),
|
||||
),
|
||||
mpb.AppendDecorators(
|
||||
decor.Elapsed(decor.ET_STYLE_GO, decor.WCSyncWidthR),
|
||||
decor.Name(" | ffmpeg stream", decor.WCSyncWidth),
|
||||
),
|
||||
mpb.BarRemoveOnComplete(),
|
||||
)
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
ticker := time.NewTicker(120 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
spin.IncrBy(1)
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
stopSpinner = func() {
|
||||
close(done)
|
||||
spin.SetTotal(-1, true)
|
||||
}
|
||||
}
|
||||
defer stopSpinner()
|
||||
|
||||
if _, err := exec.LookPath("ffmpeg"); err != nil {
|
||||
return fmt.Errorf("ffmpeg not found for manifest stream: %w", err)
|
||||
}
|
||||
@@ -369,17 +333,137 @@ func (d *Downloader) streamManifestWithFFmpeg(ctx context.Context, sourceURL, ou
|
||||
} else {
|
||||
args = append(args, "-map", "0:a:0")
|
||||
}
|
||||
args = append(args, "-c", "copy", outputPath)
|
||||
args = append(args, "-c", "copy", "-hide_banner", "-nostats", "-progress", "pipe:2", outputPath)
|
||||
|
||||
if !d.ProgressEnabled() {
|
||||
cmd := exec.CommandContext(ctx, "ffmpeg", args...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
_ = os.Remove(outputPath)
|
||||
return fmt.Errorf("ffmpeg stream copy failed: %w: %s", err, string(output))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
d.barStarted.Store(1)
|
||||
desc := shortenName(filepath.Base(outputPath), 54)
|
||||
spinner := d.progress.AddSpinner(
|
||||
0,
|
||||
mpb.PrependDecorators(
|
||||
decor.Name(desc+" ", decor.WC{W: 56, C: decor.DSyncWidth | decor.DindentRight}),
|
||||
),
|
||||
mpb.AppendDecorators(
|
||||
decor.Elapsed(decor.ET_STYLE_GO, decor.WCSyncWidthR),
|
||||
decor.Name(" | probing stream", decor.WCSyncWidth),
|
||||
),
|
||||
mpb.BarRemoveOnComplete(),
|
||||
)
|
||||
|
||||
cmd := exec.CommandContext(ctx, "ffmpeg", args...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
spinner.SetTotal(-1, true)
|
||||
return fmt.Errorf("ffmpeg stream setup failed: %w", err)
|
||||
}
|
||||
if err = cmd.Start(); err != nil {
|
||||
spinner.SetTotal(-1, true)
|
||||
return fmt.Errorf("ffmpeg stream setup failed: %w", err)
|
||||
}
|
||||
|
||||
var ffOut strings.Builder
|
||||
scanDone := make(chan scanState, 1)
|
||||
go func() {
|
||||
state := scanState{}
|
||||
shownMS := int64(0)
|
||||
newFFmpegBar := func(totalMS int64) *mpb.Bar {
|
||||
return d.progress.AddBar(
|
||||
totalMS,
|
||||
mpb.PrependDecorators(
|
||||
decor.Name(desc+" ", decor.WC{W: 56, C: decor.DSyncWidth | decor.DindentRight}),
|
||||
decor.Percentage(decor.WCSyncWidthR),
|
||||
),
|
||||
mpb.AppendDecorators(
|
||||
decor.Name(" | ", decor.WCSyncWidth),
|
||||
decor.Elapsed(decor.ET_STYLE_GO, decor.WCSyncWidthR),
|
||||
decor.Name(" | ffmpeg stream", decor.WCSyncWidth),
|
||||
),
|
||||
mpb.BarRemoveOnComplete(),
|
||||
)
|
||||
}
|
||||
var bar *mpb.Bar
|
||||
scanner := bufio.NewScanner(stderr)
|
||||
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if ffOut.Len() < 256*1024 {
|
||||
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 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 {
|
||||
if shownMS < state.totalMS {
|
||||
bar.IncrBy(int(state.totalMS - shownMS))
|
||||
}
|
||||
bar.SetTotal(state.totalMS, true)
|
||||
} else {
|
||||
spinner.SetTotal(-1, true)
|
||||
}
|
||||
state.currentMS = shownMS
|
||||
state.scanErr = scanner.Err()
|
||||
scanDone <- state
|
||||
}()
|
||||
|
||||
waitErr := cmd.Wait()
|
||||
state := <-scanDone
|
||||
|
||||
if waitErr != nil {
|
||||
_ = os.Remove(outputPath)
|
||||
return fmt.Errorf("ffmpeg stream copy failed: %w: %s", err, string(output))
|
||||
if out := strings.TrimSpace(ffOut.String()); out != "" {
|
||||
return fmt.Errorf("ffmpeg stream copy failed: %w: %s", waitErr, out)
|
||||
}
|
||||
return fmt.Errorf("ffmpeg stream copy failed: %w", waitErr)
|
||||
}
|
||||
if state.scanErr != nil {
|
||||
return fmt.Errorf("ffmpeg stream output read failed: %w", state.scanErr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type scanState struct {
|
||||
totalMS int64
|
||||
currentMS int64
|
||||
scanErr error
|
||||
}
|
||||
|
||||
func isManifestResponse(contentType string, peek []byte) bool {
|
||||
ct := strings.ToLower(contentType)
|
||||
if strings.Contains(ct, "dash+xml") || strings.Contains(ct, "mpegurl") || strings.Contains(ct, "vnd.apple.mpegurl") {
|
||||
@@ -395,6 +479,68 @@ func isManifestResponse(contentType string, peek []byte) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func parseFFmpegDurationLine(line string) (int64, bool) {
|
||||
idx := strings.Index(line, "Duration:")
|
||||
if idx < 0 {
|
||||
return 0, false
|
||||
}
|
||||
raw := strings.TrimSpace(line[idx+len("Duration:"):])
|
||||
if raw == "" {
|
||||
return 0, false
|
||||
}
|
||||
if comma := strings.Index(raw, ","); comma >= 0 {
|
||||
raw = raw[:comma]
|
||||
}
|
||||
return parseClockDurationMS(raw)
|
||||
}
|
||||
|
||||
func parseFFmpegOutTime(line string) (int64, bool) {
|
||||
if !strings.HasPrefix(line, "out_time=") {
|
||||
return 0, false
|
||||
}
|
||||
raw := strings.TrimSpace(strings.TrimPrefix(line, "out_time="))
|
||||
if raw == "" || strings.EqualFold(raw, "N/A") {
|
||||
return 0, false
|
||||
}
|
||||
return parseClockDurationMS(raw)
|
||||
}
|
||||
|
||||
func parseClockDurationMS(raw string) (int64, bool) {
|
||||
parts := strings.Split(strings.TrimSpace(raw), ":")
|
||||
if len(parts) != 3 {
|
||||
return 0, false
|
||||
}
|
||||
hours, err := strconv.ParseInt(parts[0], 10, 64)
|
||||
if err != nil || hours < 0 {
|
||||
return 0, false
|
||||
}
|
||||
minutes, err := strconv.ParseInt(parts[1], 10, 64)
|
||||
if err != nil || minutes < 0 {
|
||||
return 0, false
|
||||
}
|
||||
secPart := parts[2]
|
||||
secRaw, fracRaw, hasFrac := strings.Cut(secPart, ".")
|
||||
seconds, err := strconv.ParseInt(secRaw, 10, 64)
|
||||
if err != nil || seconds < 0 {
|
||||
return 0, false
|
||||
}
|
||||
ms := ((hours*60+minutes)*60 + seconds) * 1000
|
||||
if hasFrac {
|
||||
if len(fracRaw) > 3 {
|
||||
fracRaw = fracRaw[:3]
|
||||
}
|
||||
for len(fracRaw) < 3 {
|
||||
fracRaw += "0"
|
||||
}
|
||||
fracMS, fracErr := strconv.ParseInt(fracRaw, 10, 64)
|
||||
if fracErr != nil || fracMS < 0 {
|
||||
return 0, false
|
||||
}
|
||||
ms += fracMS
|
||||
}
|
||||
return ms, true
|
||||
}
|
||||
|
||||
const deezerBFChunkSize = 2048
|
||||
|
||||
var deezerBFIV = []byte{0, 1, 2, 3, 4, 5, 6, 7}
|
||||
|
||||
Reference in New Issue
Block a user