使用FFmpeg命令來研究它對HLS協議的支持程度是最好的方法:
ffmpeg -h muxer=hls
Muxer HLS
Muxer hls [Apple HTTP Live Streaming]:Common extensions: m3u8.Default video codec: h264.Default audio codec: aac.Default subtitle codec: webvtt.
這里面告訴我們,FFmpeg中的Muxer hls實際上是對于Apple HTTP Live Streaming的一種實現(HLS,全稱HTTP Live Streaming,是Apple公司發布的協議),這里明確說明了HLS只是一種封裝格式而與編碼無關。
默認的文件擴展名為m3u8
?,我們在瀏覽器中觀看動漫、電影的時候,可以使用工具去查看它里面的鏈接。最終,你大概率會發現這樣一種m3u8
?文件的訪問鏈接。
FFmpeg的hls muxer默認支持的視頻、音頻和字幕的編碼格式分別是:h264
?、aac
?和webvtt
?。這也就意味著如果我們想要對其他編碼格式的音頻或者視頻進行HLS封裝,那么就需要顯式地去指定這些編碼格式。需要注意的是,這些其他的編碼格式需要是HLS協議支持的編碼格式。
假設我們需要對一個MP4文件進行HLS切片,更準確地說是將MP$的封裝格式轉換成HLS的封裝格式,只不過HLS這個封裝格式是由多個音視頻文件和一個M3U8(該文件在HLS協議中被稱為Media Playlist,用作指導這些切片后的音視頻如何播放)組成的。
例子
多的不說,我這里找一個MP4文件,使用FFmpeg將其轉換成hls的封裝格式,看看會出現什么樣的結果。
這里我使用ffprobe查看該MP4文件的編碼,來明確是否需要進行轉碼操作:
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'test_input.mp4':Metadata:major_brand : isomminor_version : 512compatible_brands: isomiso2avc1mp41encoder : Lavf60.16.100Duration: 00:00:14.82, start: 0.000000, bitrate: 2662 kb/sStream #0:0[0x1](und): Video: h264 (High) (avc1 / 0x31637661), yuv420p(tv, bt709, progressive), 1920x1200 [SAR 1:1 DAR 8:5], 2489 kb/s, 60 fps, 60 tbr, 15360 tbn (default)Metadata:handler_name : VideoHandlervendor_id : [0][0][0][0]Stream #0:1[0x2](und): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 159 kb/s (default)Metadata:handler_name : SoundHandlervendor_id : [0][0][0][0]
你可以看到,視頻編碼h264
?和音頻編碼aac
?都是默認的,這意味著我們不需要轉碼。
因此,我們可以使用以下命令來進行轉封裝:
mkdir output & ffmpeg -i test_input.mp4 -c copy -f hls output/index.m3u8
使用命令來查看output
?目錄中的內容:
> tree output
output
├── index.m3u8
├── index0.ts
├── index1.ts
├── index2.ts
└── index3.ts> cat output/index.m3u8
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:4
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:4.166667,
index0.ts
#EXTINF:4.166667,
index1.ts
#EXTINF:4.166667,
index2.ts
#EXTINF:2.366667,
index3.ts
#EXT-X-ENDLIST
此刻,output
?目錄中的所有內容,組成了HLS協議的封裝格式,雖然它們由多個文件組成。
但是,當需要切片的文件變大時,index.m3u8
?的內容會和實際的切片數量對不上,原因就是:默認的hls_list_size
?的值為5
?。因此,index.m3u8文件中只會記錄最新的5個切片。使用-hls_list_size
?能夠自己指定這個值。
選項
額外指定一些選項,讓HLS的切片符合你的需求。
start_number
設置開始的序列號,默認從0開始。這里我們設置序列號為1進行切片,那么產生的結果為:
> mkdir output & ffmpeg -i test_input.mp4 -c copy -f hls -start_number 1 output/index.m3u8
> tree output
output
├── index.m3u8
├── index1.ts
├── index2.ts
├── index3.ts
└── index4.ts
注意,生成的切片文件名的序列號和m3u8文件中的序列號是對應的。
hls_time
指定切片的時間長度,單位為秒,默認值為2,類型是float
?。
> cat output/index.m3u8
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:4
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:4.166667,
index0.ts
#EXTINF:4.166667,
index1.ts
#EXTINF:4.166667,
index2.ts
#EXTINF:2.366667,
index3.ts
#EXT-X-ENDLIST
#EXTINF
?就是切片的時間長度,這里切片的時間長度為4.166667
?,這和默認值2
?不相符。這是什么原因導致的呢?我猜測,這可能是因為HLS對于每個切片的關鍵幀具有某種要求,導致了最終的切片時間按照原本視頻的關鍵幀分布來進行切片。
使用-force_key_frames "expr:gte(t,n_forced*2)"
?來讓視頻GOP大小為2秒,以此讓切片能夠按照我們所設置的參數運行:
$ ffmpeg -i test_input.mp4 \
> -f hls \
> -force_key_frames "expr:gte(t,n_forced*2)" \
> -hls_time 2 \
> output/index.m3u8$ cat output/index.m3u8
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:2
#EXT-X-MEDIA-SEQUENCE:3
#EXTINF:2.000000,
index3.ts
#EXTINF:2.000000,
index4.ts
#EXTINF:2.000000,
index5.ts
#EXTINF:2.000000,
index6.ts
#EXTINF:0.866667,
index7.ts
#EXT-X-ENDLIST
這證明我們的猜測是正確的。注意,由于-force_key_frames
?選項改變了原本的視頻幀,因此不能夠指定-c copy
?(如果指定,則會導致我們無法對原本的視頻編碼做出任何改變,-force_key_frames
?就會失效)。
你可以看到上面只記錄了5個切片,3 4 5 6 7
?,而我們的切片明明是從0開始的,并且0 1 2
?確實存在于output目錄中,但卻沒有被m3u8文件記錄,這就關系到-hls_list_size
?的使用了
hls_list_size
前面說了,由于m3u8
?這個Media Playlist只會記錄最新的幾個切片,這可能會導致播放錯誤。該選項默認值是5,當切片多于5個時,你就要考慮將其設置地大一些,防止切片錯誤。
hls_base_url
你可以指定切片的基礎路徑,比如:http://xxx.com/
?。這樣瀏覽器可以通過讀取m3u8
?文件,然后通過網絡來訪問這些切片。
比如:
> ffmpeg -i test_input.mp4 -c copy -f hls \
> -hls_base_url "http://www.aderversa.com/" \
> output/index.m3u8> cat output/index.m3u8
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:4
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:4.166667,
http://www.aderversa.com/index0.ts
#EXTINF:4.166667,
http://www.aderversa.com/index1.ts
#EXTINF:4.166667,
http://www.aderversa.com/index2.ts
#EXTINF:2.316667,
http://www.aderversa.com/index3.ts
#EXT-X-ENDLIST
該選項通常和服務器配合使用。你設置了一個URL,然后瀏覽器可以通過從服務器獲取m3u8
?文件和視頻切片文件。
遠程播放HLS視頻
在前面,我們已經知道了,HLS封裝格式會生成以下文件:
output
├── index.m3u8
├── index0.ts
├── index1.ts
├── index2.ts
└── index3.ts
播放器拿到m3u8文件,它應該能夠按照HLS協議并參考m3u8上的內容自主獲取視頻切片并播放。
這里我使用Qt6.6的QMediaPlayer
?來播放m3u8文件(這樣方便理解,實際上M3U8不包含視頻數據,它只含有該怎樣播放的信息)。
如何播放呢?抓住一個關鍵點:只要QMediaPlayer
?拿到了M3U8文件,它就能夠依照該M3U8文件播放上面的資源,而我們是不需要了解它具體如何播放的,因為播放器的開發者為我們完成了這部分工作。我們唯一需要關注的就是:如何讓QMediaPlayer
?獲取到這份M3U8文件。
這里我在網上隨便找一個M3U8文件的鏈接(隨便找個不是很正規的視頻網站一般都能夠找到,查看其HTML代碼你就能夠發現隱藏在其中的M3U8文件):
?
具體是什么URL我就不放出來了。
我們可以發現該播放器需要一個M3U8的URL才能夠播放。
我們將url=
?后面的鏈接命名為VideoURL
?,方便后續說明。
這里我們在瀏覽器中請求VideoURL
?,看看會發生什么?結果就是,瀏覽器給我們下載了一個M3U8文件。
我們使用以下Qt6.6中的代碼來播放VideoURL
?,看看能否播放成功:
int main(int argc, char *argv[])
{QApplication a(argc, argv);QMediaPlayer player;player.setSource(QUrl("https://play.modujx11.com/20250104/pZZhNChc/index.m3u8"));QVideoWidget video_widget;player.setVideoOutput(&video_widget);video_widget.show();QAudioOutput audio_output;player.setAudioOutput(&audio_output);player.play();return a.exec();
}
結果是,播放成功了。
這說明了什么呢?說明了只要服務器能夠提供一個接口,讓瀏覽器能夠通過訪問該接口URL下載到M3U8文件,且M3U8中的資源是可以被瀏覽器訪問到的,那么實現了HLS協議的播放器應該就能播放該M3U8文件。
利用上面實驗出來的特性搭建視頻平臺的一些猜想
那么,如果我們的應用程序想要通過HLS協議實現視頻遠程播放的功能,首先客戶端需要有M3U8文件并進行播放的能力;而服務器只需要提供下載M3U8文件和下載M3U8文件中對應的切片文件的接口即可。
若應用程序能夠播放的視頻是服務器端規定好的,用戶無法上傳任何視頻,那么我們在服務器端完全可以自己在相應的文件夾下使用FFmpeg命令來慢慢進行切片。
若用戶可在應用程序中上傳視頻,那么上傳完成之后,服務器端如果不追求性能和定制化,我個人認為直接調用FFmpeg的命令行程序來完成HLS的切片是沒有問題的。如果用戶上傳的視頻的編碼或者封裝格式不合適,那么要么禁止用戶上傳這類視頻;要么就在后端慢慢進行轉碼,若同一時間有大量轉碼的視頻,那么對于性能的消耗將是災難性的,因此大部分應用程序都不會允許用戶將格式不合適的視頻直接上傳到服務器端,而是讓用戶自行找方法轉碼,轉碼完成后再發送到服務器端。
用戶若是能夠上傳視頻,如果應用程序具有一定的用戶基數,那么上傳的視頻數量大概率是會逐漸增加的,對服務器的性能要求也會逐漸提高(不管是空間上還是時間上)。
應用程序可能一開始就是將這些視頻開放給所有用戶的,用戶可以通過客戶端來訪問或自己上傳的、或別人上傳的視頻數據。我們可能需要使用數據庫存業務數據 + 文件系統存視頻數據 + 數據庫和文件系統之間存在某種數據上的聯系,以此數據存儲為基礎構建服務器,然后客戶端/前端再基于服務器的接口構建符合需求的交互界面。
實現一個簡單的服務器來驗證猜想
首先,在SpringBoot中實現這樣一個Controller,用來下載服務器上的文件:
@Controller
public class FileDownloadController {private static final String FILE_DIR = "/videos";@GetMapping("/download/{dirname}/{filename:.+}")@ResponseBodypublic ResponseEntity<Resource> downloadFile(@PathVariable("dirname") String dirname,@PathVariable("filename") String filename) {String path = System.getProperty("user.dir") + FILE_DIR + "/" + dirname;Path media_list_path = Paths.get(path).resolve(filename).normalize();try {UrlResource resource = new UrlResource(media_list_path.toUri());if (resource.exists() || resource.isReadable()) {return ResponseEntity.ok().contentType(MediaType.APPLICATION_OCTET_STREAM).header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"").body(resource);}else {return ResponseEntity.notFound().build();}}catch (Exception e) {e.printStackTrace();return ResponseEntity.status(500).build();}}
}
接著,我們在指定的/videos?目錄下創建好HLS封裝的文件:
> ffmpeg -i hls_video.mp4 -f hls \
> -hls_base_url \
> http://127.0.0.1:8080/download/hlstest1/ \
> -c copy \
> -hls_list_size 1000 \
> hlstest1/index.m3u8> tree .
.
├── hls_video.mp4
└── hlstest1├── index.m3u8├── index0.ts├── index1.ts...
大致內容如上所示,然后在替換Qt原本代碼中的URL成http://localhost:8080/download/hlstest1/index.m3u8?,播放成功。
我這里的服務器運行在本機中,所以ip為localhost?。