55using Microsoft . Extensions . Logging ;
66using Microsoft . Extensions . Options ;
77using System . Diagnostics ;
8+ using System . Text ;
89
910namespace FileService . Service . Implementation
1011{
@@ -13,6 +14,86 @@ public class FfmpegService : IFfmpegService
1314 private readonly ILogger < FfmpegService > _logger ;
1415 private readonly IMinioService _minioService ;
1516 private readonly string _tempFolder ;
17+
18+ private bool HasAudioStream ( string videoPath )
19+ {
20+ var psi = new ProcessStartInfo
21+ {
22+ FileName = "ffprobe" ,
23+ Arguments = $ "-v error -select_streams a:0 -show_entries stream=codec_type -of csv=p=0 \" { videoPath } \" ",
24+ RedirectStandardOutput = true ,
25+ RedirectStandardError = true ,
26+ UseShellExecute = false ,
27+ CreateNoWindow = true
28+ } ;
29+ using var p = Process . Start ( psi ) ;
30+ string stdout = p . StandardOutput . ReadToEnd ( ) ;
31+ p . WaitForExit ( ) ;
32+ return ! string . IsNullOrWhiteSpace ( stdout ) ; // có ít nhất 1 audio stream
33+ }
34+
35+
36+ private string BuildFfmpegArgs ( string inputPath , string outputFolder , bool hasAudio )
37+ {
38+ // Scale 6 mức như cũ
39+ var filterComplex =
40+ "[0:v]split=6[v144][v240][v360][v480][v720][v1080];" +
41+ "[v144]scale=w=256:h=144:force_original_aspect_ratio=decrease:force_divisible_by=2[v144out];" +
42+ "[v240]scale=w=426:h=240:force_original_aspect_ratio=decrease:force_divisible_by=2[v240out];" +
43+ "[v360]scale=w=640:h=360:force_original_aspect_ratio=decrease:force_divisible_by=2[v360out];" +
44+ "[v480]scale=w=854:h=480:force_original_aspect_ratio=decrease:force_divisible_by=2[v480out];" +
45+ "[v720]scale=w=1280:h=720:force_original_aspect_ratio=decrease:force_divisible_by=2[v720out];" +
46+ "[v1080]scale=w=1920:h=1080:force_original_aspect_ratio=decrease:force_divisible_by=2[v1080out]" ;
47+
48+ // map + encode per-variant
49+ var maps = new StringBuilder ( ) ;
50+ // Video bitrates/profiles tương ứng index
51+ var v = new ( string label , string br , string profile ) [ ]
52+ {
53+ ( "[v144out]" , "300k" , "baseline" ) ,
54+ ( "[v240out]" , "700k" , "baseline" ) ,
55+ ( "[v360out]" , "1000k" , "main" ) ,
56+ ( "[v480out]" , "1500k" , "main" ) ,
57+ ( "[v720out]" , "2500k" , "main" ) ,
58+ ( "[v1080out]" , "4000k" , "high" ) ,
59+ } ;
60+
61+ for ( int i = 0 ; i < v . Length ; i ++ )
62+ {
63+ maps . Append (
64+ $ " -map \" { v [ i ] . label } \" -c:v:{ i } libx264 -b:v:{ i } { v [ i ] . br } -profile:v:{ i } { v [ i ] . profile } " +
65+ $ "-g 48 -keyint_min 48 -sc_threshold 0 -pix_fmt yuv420p"
66+ ) ;
67+
68+ if ( hasAudio )
69+ {
70+ // audio từ input (nếu có kênh), downmix stereo cho tương thích
71+ maps . Append ( $ " -map 0:a:0? -c:a:{ i } aac -b:a:{ i } 128k -ac 2 -ar 48000") ;
72+ }
73+ }
74+
75+ // var_stream_map tương ứng
76+ var varStreamMap = hasAudio
77+ ? "\" v:0,a:0 v:1,a:1 v:2,a:2 v:3,a:3 v:4,a:4 v:5,a:5\" "
78+ : "\" v:0 v:1 v:2 v:3 v:4 v:5\" " ;
79+
80+ // HLS options (giữ output như cũ, thêm independent_segments cho keyframe boundary)
81+ var hls =
82+ $ "-f hls -hls_time 6 -hls_list_size 0 -hls_flags independent_segments " +
83+ $ "-var_stream_map { varStreamMap } " +
84+ $ "-master_pl_name master.m3u8 " +
85+ $ "-hls_segment_filename \" { outputFolder } /v%v/segment%d.ts\" " +
86+ $ "\" { outputFolder } /v%v/playlist.m3u8\" ";
87+
88+ // Nếu hoàn toàn không có audio, loại phụ đề nếu có để tránh rắc rối
89+ var extra = hasAudio ? "" : " -sn" ;
90+
91+ // max_muxing_queue_size để tránh lỗi queue với vài file lạ
92+ var safety = " -max_muxing_queue_size 1024" ;
93+
94+ return
95+ $ "-i \" { inputPath } \" -filter_complex \" { filterComplex } \" { maps } { safety } { extra } { hls } ";
96+ }
1697
1798 public FfmpegService ( ILogger < FfmpegService > logger , IOptions < FfmpegSettings > settings , IMinioService minioService )
1899 {
@@ -88,76 +169,42 @@ public async Task<VideoProcessResultModel> ProcessVideoAsync(IFormFile videoFile
88169
89170 try
90171 {
172+ // tạo thư mục variant
91173 for ( int i = 0 ; i <= 5 ; i ++ )
92174 {
93- var resolutionFolder = Path . Combine ( outputFolder , $ "v{ i } ") ;
94- Directory . CreateDirectory ( resolutionFolder ) ;
175+ Directory . CreateDirectory ( Path . Combine ( outputFolder , $ "v{ i } ") ) ;
95176 }
96177
97- // Save input video to temp file
178+ // Save input
98179 await using ( var stream = new FileStream ( tempInputPath , FileMode . Create ) )
99180 {
100181 await videoFile . CopyToAsync ( stream ) ;
101182 }
102183
103- // Create thumbnail
184+ // Thumbnail
104185 var thumbnailArgs = $ "-ss 00:00:01 -i \" { tempInputPath } \" -frames:v 1 -q:v 2 \" { thumbnailPath } \" ";
105186 await RunFfmpegAsync ( thumbnailArgs ) ;
106187
107- //FFmpeg CLI arguments for HLS Adaptive Bitrate
108- var ffmpegArgs = $ "-i \" { tempInputPath } \" " +
109- "-filter_complex " +
110- "\" [0:v]split=6[v144][v240][v360][v480][v720][v1080]; " +
111- "[v144]scale=w=256:h=144:force_original_aspect_ratio=decrease:force_divisible_by=2[v144out]; " +
112- "[v240]scale=w=426:h=240:force_original_aspect_ratio=decrease:force_divisible_by=2[v240out]; " +
113- "[v360]scale=w=640:h=360:force_original_aspect_ratio=decrease:force_divisible_by=2[v360out]; " +
114- "[v480]scale=w=854:h=480:force_original_aspect_ratio=decrease:force_divisible_by=2[v480out]; " +
115- "[v720]scale=w=1280:h=720:force_original_aspect_ratio=decrease:force_divisible_by=2[v720out]; " +
116- "[v1080]scale=w=1920:h=1080:force_original_aspect_ratio=decrease:force_divisible_by=2[v1080out]\" " +
117-
118- "-map \" [v144out]\" -map 0:a -c:v:0 libx264 -b:v:0 300k -profile:v:0 baseline -c:a:0 aac -b:a:0 96k " +
119- "-map \" [v240out]\" -map 0:a -c:v:1 libx264 -b:v:1 700k -profile:v:1 baseline -c:a:1 aac -b:a:1 96k " +
120- "-map \" [v360out]\" -map 0:a -c:v:2 libx264 -b:v:2 1000k -profile:v:2 main -c:a:2 aac -b:a:2 128k " +
121- "-map \" [v480out]\" -map 0:a -c:v:3 libx264 -b:v:3 1500k -profile:v:3 main -c:a:3 aac -b:a:3 128k " +
122- "-map \" [v720out]\" -map 0:a -c:v:4 libx264 -b:v:4 2500k -profile:v:4 main -c:a:4 aac -b:a:4 128k " +
123- "-map \" [v1080out]\" -map 0:a -c:v:5 libx264 -b:v:5 4000k -profile:v:5 high -c:a:5 aac -b:a:5 128k " +
124-
125- "-f hls -var_stream_map " +
126- "\" v:0,a:0 v:1,a:1 v:2,a:2 v:3,a:3 v:4,a:4 v:5,a:5\" " +
127- $ "-master_pl_name master.m3u8 -hls_time 6 -hls_list_size 0 " +
128- $ "-hls_segment_filename \" { outputFolder } /v%v/segment%d.ts\" " +
129- $ "\" { outputFolder } /v%v/playlist.m3u8\" ";
130-
188+ // >>> PHÁT HIỆN AUDIO & BUILD LỆNH PHÙ HỢP <<<
189+ bool hasAudio = HasAudioStream ( tempInputPath ) ;
190+ var ffmpegArgs = BuildFfmpegArgs ( tempInputPath , outputFolder , hasAudio ) ;
131191 await RunFfmpegAsync ( ffmpegArgs ) ;
132192
133193 // Upload thumbnail
134194 var thumbKey = $ "thumbnails/{ fileId } .jpg";
135195 await using ( var thumbStream = File . OpenRead ( thumbnailPath ) )
136- {
137196 await _minioService . UploadStreamAsync ( thumbKey , thumbStream , "image/jpeg" ) ;
138- }
139197
140198 // Upload master playlist
141199 var baseVideoPath = $ "videos/{ fileId } /master.m3u8";
142200 await using ( var masterStream = File . OpenRead ( masterPlaylistPath ) )
143- {
144201 await _minioService . UploadStreamAsync ( baseVideoPath , masterStream , "application/x-mpegURL" ) ;
145- }
146202
147- // Upload HLS segments and playlists
203+ // Upload từng playlist + segment
148204 for ( int i = 0 ; i <= 5 ; i ++ )
149205 {
150206 var segmentFolder = Path . Combine ( outputFolder , $ "v{ i } ") ;
151- var segmentFiles = Directory . GetFiles ( segmentFolder , "segment*.ts" ) ;
152207
153- foreach ( var segmentFile in segmentFiles )
154- {
155- var segKey = $ "videos/{ fileId } /v{ i } /{ Path . GetFileName ( segmentFile ) } ";
156- await using var segStream = File . OpenRead ( segmentFile ) ;
157- await _minioService . UploadStreamAsync ( segKey , segStream , "video/MP2T" ) ;
158- }
159-
160- // Upload playlist.m3u8 for each resolution folder
161208 var playlistFile = Path . Combine ( segmentFolder , "playlist.m3u8" ) ;
162209 if ( File . Exists ( playlistFile ) )
163210 {
@@ -166,9 +213,14 @@ public async Task<VideoProcessResultModel> ProcessVideoAsync(IFormFile videoFile
166213 await _minioService . UploadStreamAsync ( playlistKey , playlistStream , "application/x-mpegURL" ) ;
167214 }
168215
216+ foreach ( var segmentFile in Directory . GetFiles ( segmentFolder , "segment*.ts" ) )
217+ {
218+ var segKey = $ "videos/{ fileId } /v{ i } /{ Path . GetFileName ( segmentFile ) } ";
219+ await using var segStream = File . OpenRead ( segmentFile ) ;
220+ await _minioService . UploadStreamAsync ( segKey , segStream , "video/mp2t" ) ; // <— sửa MIME
221+ }
169222 }
170223
171- // get duration using ffprobe
172224 var duration = GetVideoDuration ( tempInputPath ) ;
173225 var thumbnailUrl = await _minioService . GetPublicFileUrlAsync ( thumbKey ) ;
174226 var hlsUrl = await _minioService . GetPublicFileUrlAsync ( baseVideoPath ) ;
@@ -186,10 +238,7 @@ public async Task<VideoProcessResultModel> ProcessVideoAsync(IFormFile videoFile
186238 Console . WriteLine ( $ "Video processing failed: { ex . Message } ") ;
187239 return new VideoProcessResultModel
188240 {
189- ThumbnailUrl = null ,
190- Duration = null ,
191- Status = "failed" ,
192- HlsUrl = null
241+ Status = "failed"
193242 } ;
194243 }
195244 finally
0 commit comments