55 "os"
66 "os/exec"
77 "path/filepath"
8+ "strings"
89 "sync"
910 "sync/atomic"
1011
@@ -17,19 +18,31 @@ func (d *M3U8Downloader) Download(job *utils.DanzoJob) error {
1718 if err := os .MkdirAll (tempDir , 0755 ); err != nil {
1819 return fmt .Errorf ("error creating temp directory: %v" , err )
1920 }
20- defer os .RemoveAll (tempDir )
21+ var downloadErr error
22+ defer func () {
23+ if downloadErr == nil {
24+ os .RemoveAll (tempDir )
25+ } else {
26+ log .Warn ().Str ("op" , "live-stream/download" ).Msgf ("Preserving segments in %s due to error" , tempDir )
27+ }
28+ }()
29+
2130 client := utils .NewDanzoHTTPClient (job .HTTPClientConfig )
2231 log .Debug ().Str ("op" , "live-stream/download" ).Msgf ("Fetching manifest from %s" , job .URL )
2332 manifestContent , err := getM3U8Contents (job .URL , client )
2433 if err != nil {
25- return fmt .Errorf ("error fetching manifest: %v" , err )
34+ downloadErr = fmt .Errorf ("error fetching manifest: %v" , err )
35+ return downloadErr
2636 }
27- segmentURLs , err := processM3U8Content (manifestContent , job .URL , client )
37+ m3u8Info , err := parseM3U8Content (manifestContent , job .URL , client )
2838 if err != nil {
29- return fmt .Errorf ("error processing manifest: %v" , err )
39+ downloadErr = fmt .Errorf ("error processing manifest: %v" , err )
40+ return downloadErr
3041 }
42+ segmentURLs := m3u8Info .SegmentURLs
3143 if len (segmentURLs ) == 0 {
32- return fmt .Errorf ("no segments found in manifest" )
44+ downloadErr = fmt .Errorf ("no segments found in manifest" )
45+ return downloadErr
3346 }
3447 log .Info ().Str ("op" , "live-stream/download" ).Msgf ("Found %d segments to download" , len (segmentURLs ))
3548
@@ -47,20 +60,43 @@ func (d *M3U8Downloader) Download(job *utils.DanzoJob) error {
4760 job .Metadata ["segmentSizes" ] = segmentSizes
4861 log .Debug ().Str ("op" , "live-stream/download" ).Msgf ("Total estimated size: %s" , utils .FormatBytes (uint64 (totalSize )))
4962
63+ // Detect fMP4 format
64+ isFMP4 := detectFMP4Format (job .URL , segmentURLs )
65+ if isFMP4 {
66+ log .Debug ().Str ("op" , "live-stream/download" ).Msg ("Detected fMP4 format segments" )
67+ }
68+
5069 log .Info ().Str ("op" , "live-stream/download" ).Msg ("Starting parallel download of segments" )
51- segmentFiles , err := downloadSegmentsParallel (segmentURLs , tempDir , job .Connections , client , job .ProgressFunc , totalSize )
70+ segmentFiles , err := downloadSegmentsParallel (segmentURLs , tempDir , job .Connections , client , job .ProgressFunc , totalSize , isFMP4 )
5271 if err != nil {
53- return fmt .Errorf ("error downloading segments: %v" , err )
72+ downloadErr = fmt .Errorf ("error downloading segments: %v" , err )
73+ return downloadErr
5474 }
5575 log .Info ().Str ("op" , "live-stream/download" ).Msg ("All segments downloaded, merging with ffmpeg" )
56- if err := mergeSegments (segmentFiles , job .OutputPath ); err != nil {
57- return fmt .Errorf ("error merging segments: %v" , err )
76+ if err := mergeSegments (segmentFiles , job .OutputPath , isFMP4 , m3u8Info .InitSegment , tempDir , client ); err != nil {
77+ downloadErr = fmt .Errorf ("error merging segments: %v" , err )
78+ return downloadErr
5879 }
5980 log .Info ().Str ("op" , "live-stream/download" ).Msg ("Segments merged successfully" )
6081 return nil
6182}
6283
63- func downloadSegmentsParallel (segmentURLs []string , outputDir string , numWorkers int , client * utils.DanzoHTTPClient , progressFunc func (int64 , int64 ), totalSize int64 ) ([]string , error ) {
84+ func detectFMP4Format (manifestURL string , segmentURLs []string ) bool {
85+ if strings .Contains (manifestURL , "/fmp4/" ) || strings .Contains (manifestURL , "frag" ) {
86+ return true
87+ }
88+ if len (segmentURLs ) > 0 {
89+ firstSegment := segmentURLs [0 ]
90+ if strings .Contains (firstSegment , "/fmp4/" ) ||
91+ strings .Contains (firstSegment , ".m4s" ) ||
92+ strings .Contains (firstSegment , "frag" ) {
93+ return true
94+ }
95+ }
96+ return false
97+ }
98+
99+ func downloadSegmentsParallel (segmentURLs []string , outputDir string , numWorkers int , client * utils.DanzoHTTPClient , progressFunc func (int64 , int64 ), totalSize int64 , isFMP4 bool ) ([]string , error ) {
64100 var downloadedFiles []string
65101 var mu sync.Mutex
66102 var totalDownloaded int64
@@ -75,14 +111,18 @@ func downloadSegmentsParallel(segmentURLs []string, outputDir string, numWorkers
75111 }
76112 close (jobCh )
77113 downloadedFiles = make ([]string , len (segmentURLs ))
114+ ext := ".ts"
115+ if isFMP4 {
116+ ext = ".m4s"
117+ }
78118
79119 var wg sync.WaitGroup
80120 for range numWorkers {
81121 wg .Add (1 )
82122 go func () {
83123 defer wg .Done ()
84124 for job := range jobCh {
85- outputPath := filepath .Join (outputDir , fmt .Sprintf ("segment_%04d.ts " , job .index ))
125+ outputPath := filepath .Join (outputDir , fmt .Sprintf ("segment_%04d%s " , job .index , ext ))
86126 size , err := downloadSegment (job .url , outputPath , client )
87127 if err != nil {
88128 mu .Lock ()
@@ -110,7 +150,14 @@ func downloadSegmentsParallel(segmentURLs []string, outputDir string, numWorkers
110150 return downloadedFiles , nil
111151}
112152
113- func mergeSegments (segmentFiles []string , outputPath string ) error {
153+ func mergeSegments (segmentFiles []string , outputPath string , isFMP4 bool , initSegment string , tempDir string , client * utils.DanzoHTTPClient ) error {
154+ if isFMP4 {
155+ return mergeFMP4Segments (segmentFiles , outputPath , initSegment , tempDir , client )
156+ }
157+ return mergeTSSegments (segmentFiles , outputPath )
158+ }
159+
160+ func mergeTSSegments (segmentFiles []string , outputPath string ) error {
114161 tempListFile := filepath .Join (filepath .Dir (outputPath ), ".segment_list.txt" )
115162 f , err := os .Create (tempListFile )
116163 if err != nil {
@@ -133,6 +180,66 @@ func mergeSegments(segmentFiles []string, outputPath string) error {
133180 log .Debug ().Str ("op" , "live-stream/download" ).Msgf ("Executing ffmpeg command: %s" , cmd .String ())
134181 output , err := cmd .CombinedOutput ()
135182 if err != nil {
183+ log .Error ().Str ("op" , "live-stream/download" ).Msgf ("FFmpeg output:\n %s" , string (output ))
184+ return fmt .Errorf ("ffmpeg error: %v\n Output: %s" , err , string (output ))
185+ }
186+ return nil
187+ }
188+
189+ func mergeFMP4Segments (segmentFiles []string , outputPath string , initSegment string , tempDir string , client * utils.DanzoHTTPClient ) error {
190+ tempConcatFile := filepath .Join (filepath .Dir (outputPath ), ".concat_temp.m4s" )
191+ defer os .Remove (tempConcatFile )
192+ log .Debug ().Str ("op" , "live-stream/download" ).Msgf ("Concatenating %d fMP4 segments" , len (segmentFiles ))
193+ outFile , err := os .Create (tempConcatFile )
194+ if err != nil {
195+ return fmt .Errorf ("error creating temp concat file: %v" , err )
196+ }
197+ if initSegment != "" {
198+ log .Debug ().Str ("op" , "live-stream/download" ).Msg ("Downloading init segment" )
199+ initPath := filepath .Join (tempDir , "init.mp4" )
200+ _ , err := downloadSegment (initSegment , initPath , client )
201+ if err != nil {
202+ outFile .Close ()
203+ return fmt .Errorf ("error downloading init segment: %v" , err )
204+ }
205+ initData , err := os .ReadFile (initPath )
206+ if err != nil {
207+ outFile .Close ()
208+ return fmt .Errorf ("error reading init segment: %v" , err )
209+ }
210+ if _ , err := outFile .Write (initData ); err != nil {
211+ outFile .Close ()
212+ return fmt .Errorf ("error writing init segment: %v" , err )
213+ }
214+ log .Debug ().Str ("op" , "live-stream/download" ).Msgf ("Init segment written (%d bytes)" , len (initData ))
215+ }
216+ for i , segmentFile := range segmentFiles {
217+ data , err := os .ReadFile (segmentFile )
218+ if err != nil {
219+ outFile .Close ()
220+ return fmt .Errorf ("error reading segment %d: %v" , i , err )
221+ }
222+ if _ , err := outFile .Write (data ); err != nil {
223+ outFile .Close ()
224+ return fmt .Errorf ("error writing segment %d: %v" , i , err )
225+ }
226+ }
227+ outFile .Close ()
228+
229+ log .Debug ().Str ("op" , "live-stream/download" ).Msg ("Remuxing concatenated fMP4 segments" )
230+ cmd := exec .Command (
231+ "ffmpeg" ,
232+ "-i" , tempConcatFile ,
233+ "-c" , "copy" ,
234+ "-movflags" , "+faststart" ,
235+ "-y" ,
236+ outputPath ,
237+ )
238+ log .Debug ().Str ("op" , "live-stream/download" ).Msgf ("Executing ffmpeg command: %s" , cmd .String ())
239+ output , err := cmd .CombinedOutput ()
240+ if err != nil {
241+ log .Error ().Str ("op" , "live-stream/download" ).Msgf ("FFmpeg failed with error: %v" , err )
242+ log .Error ().Str ("op" , "live-stream/download" ).Msgf ("FFmpeg output:\n %s" , string (output ))
136243 return fmt .Errorf ("ffmpeg error: %v\n Output: %s" , err , string (output ))
137244 }
138245 return nil
0 commit comments