fix: ctx cancellation
This commit is contained in:
@@ -24,6 +24,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
defaultClientTimeout = 30 * time.Second
|
defaultClientTimeout = 30 * time.Second
|
||||||
|
vodSegmentTimeout = 5 * time.Minute // Longer timeout for VOD segments which can be large
|
||||||
maxRedirects = 5
|
maxRedirects = 5
|
||||||
playlistPollInterval = 2 * time.Second // Poll interval for livestreams
|
playlistPollInterval = 2 * time.Second // Poll interval for livestreams
|
||||||
)
|
)
|
||||||
@@ -193,37 +194,66 @@ func downloadPlaylist(ctx context.Context, client *http.Client, parsed *url.URL,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For VOD (Video on Demand), use temp file and convert to MP4
|
// For VOD (Video on Demand), use temp file and convert to MP4
|
||||||
|
// Create a context without deadline for VOD downloads to avoid premature cancellation
|
||||||
|
// Preserve cancellation from parent context but remove any deadline
|
||||||
|
vodCtx, vodCancel := context.WithCancel(context.Background())
|
||||||
|
defer vodCancel()
|
||||||
|
|
||||||
|
// If parent context is cancelled, cancel VOD context too
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
vodCancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Create a client with longer timeout for VOD segments
|
||||||
|
vodClient := &http.Client{Timeout: vodSegmentTimeout}
|
||||||
|
|
||||||
tempTS, err := os.CreateTemp("", "sdl-*.ts")
|
tempTS, err := os.CreateTemp("", "sdl-*.ts")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create temp file: %w", err)
|
return fmt.Errorf("create temp file: %w", err)
|
||||||
}
|
}
|
||||||
tempPath := tempTS.Name()
|
tempPath := tempTS.Name()
|
||||||
defer func() {
|
defer os.Remove(tempPath)
|
||||||
tempTS.Close()
|
|
||||||
os.Remove(tempPath)
|
|
||||||
}()
|
|
||||||
|
|
||||||
segments := collectSegments(mediaPlaylist)
|
segments := collectSegments(mediaPlaylist)
|
||||||
totalSegments := len(segments)
|
totalSegments := len(segments)
|
||||||
fmt.Fprintf(os.Stderr, "Downloading %d segments...\n", totalSegments)
|
fmt.Fprintf(os.Stderr, "Downloading %d segments...\n", totalSegments)
|
||||||
|
|
||||||
for i, segment := range segments {
|
for i, segment := range segments {
|
||||||
if err := downloadSegment(ctx, client, finalURL, segment, tempTS); err != nil {
|
if err := downloadSegment(vodCtx, vodClient, finalURL, segment, tempTS); err != nil {
|
||||||
|
tempTS.Close()
|
||||||
return fmt.Errorf("download segment %d: %w", i, err)
|
return fmt.Errorf("download segment %d: %w", i, err)
|
||||||
}
|
}
|
||||||
// Show progress: segment number, total, and percentage
|
// Show progress: segment number, total, and percentage
|
||||||
progress := float64(i+1) / float64(totalSegments) * 100
|
progress := float64(i+1) / float64(totalSegments) * 100
|
||||||
fmt.Fprintf(os.Stderr, "\rProgress: %d/%d segments (%.1f%%)", i+1, totalSegments, progress)
|
fmt.Fprintf(os.Stderr, "\rProgress: %d/%d segments (%.1f%%)", i+1, totalSegments, progress)
|
||||||
|
os.Stderr.Sync() // Flush progress output
|
||||||
}
|
}
|
||||||
fmt.Fprintf(os.Stderr, "\n")
|
fmt.Fprintf(os.Stderr, "\n")
|
||||||
|
|
||||||
if _, err := tempTS.Seek(0, io.SeekStart); err != nil {
|
// Flush and sync the file to ensure all data is written to disk
|
||||||
return fmt.Errorf("rewind temp file: %w", err)
|
if err := tempTS.Sync(); err != nil {
|
||||||
|
tempTS.Close()
|
||||||
|
return fmt.Errorf("sync temp file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the file before conversion
|
||||||
|
if err := tempTS.Close(); err != nil {
|
||||||
|
return fmt.Errorf("close temp file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the file exists and has content
|
||||||
|
stat, err := os.Stat(tempPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stat temp file: %w", err)
|
||||||
|
}
|
||||||
|
if stat.Size() == 0 {
|
||||||
|
return fmt.Errorf("downloaded file is empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
mp4Name := ensureMP4Extension(name)
|
mp4Name := ensureMP4Extension(name)
|
||||||
fmt.Fprintf(os.Stderr, "Converting to MP4...\n")
|
fmt.Fprintf(os.Stderr, "Converting to MP4 (%.2f MB)...\n", float64(stat.Size())/1024/1024)
|
||||||
if err := transmuxToMP4(ctx, tempPath, mp4Name); err != nil {
|
if err := transmuxToMP4(vodCtx, tempPath, mp4Name); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Fprintf(os.Stderr, "Complete: %s\n", mp4Name)
|
fmt.Fprintf(os.Stderr, "Complete: %s\n", mp4Name)
|
||||||
@@ -642,14 +672,50 @@ func transmuxToMP4(ctx context.Context, tsPath, mp4Path string) error {
|
|||||||
return fmt.Errorf("ensure output directory: %w", err)
|
return fmt.Errorf("ensure output directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "ffmpeg", "-y", "-i", tsPath, "-c", "copy", "-movflags", "+faststart", mp4Path)
|
// Try with copy first (fastest), fall back to re-encoding if needed
|
||||||
|
// Use -fflags +genpts to generate presentation timestamps if missing
|
||||||
|
// Use -err_detect ignore_err to be more tolerant of minor errors
|
||||||
|
cmd := exec.CommandContext(ctx, "ffmpeg",
|
||||||
|
"-y",
|
||||||
|
"-fflags", "+genpts",
|
||||||
|
"-err_detect", "ignore_err",
|
||||||
|
"-i", tsPath,
|
||||||
|
"-c", "copy",
|
||||||
|
"-movflags", "+faststart",
|
||||||
|
mp4Path)
|
||||||
cmd.Stdout = io.Discard
|
cmd.Stdout = io.Discard
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
cmd.Stderr = &stderr
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
os.Remove(mp4Path)
|
// If copy fails, try with re-encoding as fallback
|
||||||
msg := strings.TrimSpace(stderr.String())
|
msg := strings.TrimSpace(stderr.String())
|
||||||
|
if strings.Contains(msg, "could not find corresponding") || strings.Contains(msg, "error reading header") {
|
||||||
|
// Try re-encoding instead of copy
|
||||||
|
fmt.Fprintf(os.Stderr, "Copy mode failed, trying re-encode...\n")
|
||||||
|
cmd2 := exec.CommandContext(ctx, "ffmpeg",
|
||||||
|
"-y",
|
||||||
|
"-fflags", "+genpts",
|
||||||
|
"-err_detect", "ignore_err",
|
||||||
|
"-i", tsPath,
|
||||||
|
"-c:v", "libx264",
|
||||||
|
"-c:a", "aac",
|
||||||
|
"-movflags", "+faststart",
|
||||||
|
mp4Path)
|
||||||
|
cmd2.Stdout = io.Discard
|
||||||
|
var stderr2 bytes.Buffer
|
||||||
|
cmd2.Stderr = &stderr2
|
||||||
|
if err2 := cmd2.Run(); err2 != nil {
|
||||||
|
os.Remove(mp4Path)
|
||||||
|
msg2 := strings.TrimSpace(stderr2.String())
|
||||||
|
if msg2 != "" {
|
||||||
|
return fmt.Errorf("ffmpeg (re-encode): %s", msg2)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("ffmpeg (re-encode): %w", err2)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
os.Remove(mp4Path)
|
||||||
if msg != "" {
|
if msg != "" {
|
||||||
return fmt.Errorf("ffmpeg: %s", msg)
|
return fmt.Errorf("ffmpeg: %s", msg)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user