
FFmpeg 是一套可以用來記錄、轉換數字音頻、視頻,并能將其轉化為流的開源計算機程式,它提供了錄制、轉換以及流化音視頻的完整解決方案,
官方下載網站 http://www.ffmpeg.org/download.html,下載解壓縮后請配置環境,
一、MP4 轉 M3U8
M3U8 是 Unicode 版本的 M3U,用 UTF-8 編碼,”M3U” 和 “M3U8” 檔案都是蘋果公司使用的 HTTP Live Streaming(HLS) 協議格式的基礎,這種協議格式可以在 iPhone 和 Macbook 等設備播放,
簡單來說,m3u8是一個視頻格式,就是將一個視頻分成很多的小部分,這樣方便視頻的加載,
1、操作簡單,但效率低
ffmpeg -i input.mp4 -c:v libx264 -c:a aac -strict -2 -f hls -hls_list_size 2 -hls_time 15 output.m3u8
生成的效果是:
將 input.mp4 視頻檔案每 15 秒生成一個 ts 檔案,最后生成一個 m3u8 檔案,m3u8 檔案是 ts 的索引檔案,
我們直接用 VLC media player 等播放軟體是可以直接打開 m3u8 檔案,像播放 mp4 一樣,
默認的每片長度為 2 秒,m3u8 檔案中默認只保存最新的 5 條片的資訊,導致最后播放的時候只能播最后的一小部分(直播的時候特別注意),
更多引數請看檔案:ffmpeg.org/ffmpeg.html#Video-Options
-hls_time n 設定每片的長度,默認值為 2,單位為秒,
-hls_list_size n 設定播放串列保存的最多條目,設定為 0 會保存有所片資訊,默認值為5,
-hls_wrap n 設定多少片之后開始覆寫,如果設定為0則不會覆寫,默認值為0,這個選項能夠避免在磁盤上存盤過多的 片,而且能夠限制寫入磁盤的最多的片的數量,
-hls_start_number n 設定播放串列中 sequence number 的值為 number,默認值為 0,
注意:播放串列的 sequence number 對每個 segment 來說都必須是唯一的,而且它不能和片的檔案名(當使用 wrap 選項時,檔案名有可能會重復使用)混淆,
2、效率優化版,提升效率
TS 檔案是一種媒體的擴展名,它是日本高清攝像機拍攝下進行的封裝格式,MPEG2-TS(Transport Stream“傳輸流”;又稱TS、TP、MPEG-TS 或 M2T)是用于音效、影像與資料的通信協定,最早應用于DVD的實時傳送節目,MPEG2-TS格式的特點就是要求從視頻流的任一片段開始都是可以獨立解碼的,
# 1.視頻整體轉碼ts
ffmpeg -y -i music.mp4 -vcodec copy -acodec copy -vbsf h264_mp4toannexb out\music.ts
# 2. ts 檔案切片
ffmpeg -i music.ts -c copy -map 0 -f segment -segment_list out\music.m3u8 -segment_time 10 out\15s_%3d.ts
3、hls_time 切片時間不準確的問題
播放 m3u8 的 ts 切片,必須要完整的下載一個 ts 切片,才能夠播放,設定hls_time 的時間間隔越短越好( 根據實際情況來 ),實際程序中設定切片時間間隔為 2 秒,呼叫如下指令:
ffmpeg -i test.mp4 -c:v libx264 -c:a aac -strict -2 -f hls -hls_time 2 index.m3u8
但沒有按照引數輸入,進行切片,
原因:
ts 檔案的切割,還跟原檔案視頻的 GOP 大小有關系(也就是兩個 I 幀之間的時間間隔),因為任何一個 ts 分片第一幀必須是I幀,否則無法最快播放,并且第一幀不是 I 幀,對于播放器也是沒有任何的意義,直接被播放器扔掉,任何一個視頻流必須在獲取到第一個I幀才能成功解碼出圖片,雖然指定了 1 秒切割一個 ts 檔案,實際上,由于原視頻流可能好幾秒才有一個 I 幀,所以必須等到下一個 I 幀,才會重新開始切片,
解決:
既然知道要1秒產生一個ts分片,那就必須產生切片的程序中,強制一秒中產生一個關鍵幀,
設定關鍵幀間隔,設定間隔為 2 秒的引數如下:-force_key_frames "expr:gte(t,n_forced*2)
“
完整指令如:
ffmpeg -i test.mp4 -force_key_frames "expr:gte(t,n_forced*2)" -strict -2 -c:a aac -c:v libx264 -hls_time 2 -f hls index.m3u8
4、m3u8 格式決議
完整的 m3u8 檔案有三部分:
- index.m3u8,保存視頻的基本資訊和分段檔案順序;
- key,如果視頻加密,保存密鑰;
- data檔案,其他都是視頻的資料檔案,
具體內容決議:
#EXTM3U
,是檔案開始#EXT-X-VERSION
,標識HLS的協議版本號;#EXT-X-TARGETDURATION
,表示每個視頻分段最大的時長(單位秒);#EXT-X-MEDIA-SEQUENCE
,表示播放串列第一個 URL 片段檔案的序列號;#EXT-X-PLAYLIST-TYPE
,表明流媒體型別;#EXT-X-KEY
,加密方式,這里加密方式為AES-128
,同時指定IV
,在解密時需要;#EXTINF
,表示其后 URL 指定的媒體片段時長(單位為秒),
二、播放演示

HLS 的作業原理是把整個流分成一個個小的基于 HTTP 的檔案來下載,每次只下載一些,
當媒體流正在播放時,客戶端可以選擇從許多不同的備用源中以不同的速率下載同樣的資源,允許流媒體會話適應不同的資料速率,
在開始一個流媒體會話時,客戶端會下載一個包含元資料的 extended M3U (m3u8) playlist檔案,用于尋找可用的媒體流,
HLS 只請求基本的 HTTP 報文,與實時傳輸協議(RTP)不同,HLS 可以穿過任何允許 HTTP 資料通過的防火墻或者代理服務器,
它也很容易使用內容分發網路來傳輸媒體流,
video.js 播放 hls 示例
https://xushanxiang.com/demo/ffmpeg/video_hls.html
hls.js 播放示例
https://xushanxiang.com/demo/ffmpeg/hls_js.html
三、m3u8(ts) 合并為 MP4
遠程檔案
ffmpeg -i “https://xushanxiang.com/demo/ffmpeg/hls265/output.m3u8” -vcodec copy -acodec copy -absf aac_adtstoasc output.mp4
本地檔案
1、打開cmd
2、輸入指令,按照檔案的實際路徑合并
合并成 ts
檔案 copy /b F:\f\*.ts E:\f\new.ts
合并成 MP4
檔案 copy /b F:\f\*.ts E:\f\new.MP4
而通過 ffmpeg 命令如下:
直接轉:
ffmpeg -i new.ts -c copy -map 0:v -map 0:a output.mp4
指定音頻流(一般用這個):
ffmpeg -i new.ts -c copy -map 0:v -map 0:a -bsf:a aac_adtstoasc output.mp4
重編碼視頻:
ffmpeg -y -i new.ts -c:v libx264 -c:a copy -bsf:a aac_adtstoasc output.mp4
php實作代碼
$url = 'https://******.m3u8?Expires=1585381145&OSSAccessKeyId=******&Signature=******';
$ts_content = file_get_contents($url);
$ts_content = explode(',', $ts_content);
$ts_file = array();
foreach ($ts_content as $key => $value) {
if($key == 0) continue;
$value = trim($value);
$ts_file[] = substr($value, 0, strpos($value, '.ts') + 3);
}
$url_prefix = substr($url, 0, strpos($url, '.m3u8'));
$url_prefix = substr($url, 0, strrpos($url, '/') + 1);
$file_content = '';
foreach ($ts_file as $key => $value) {
$file_content .= file_get_contents($url_prefix . $value);
}
file_put_contents('tmp_out.ts', $file_content);
// FFMPEG_PATH 是你自己解壓ffmpeg的bin路徑,例如我的是F:/ffmpeg/bin/
exec(FFMPEG_PATH . "ffmpeg -i tmp_out.ts tmp_out.mp4");
Python實作代碼
目錄結構
./
|-- m3u8.py
|-- result
|-- 檔案1
|-- key
|-- index.m3u8
|-- data...
|-- 檔案2
|-- ...
import os
import sys
import time
from Crypto.Cipher import AES
def fileList(findex):
rpath = os.path.dirname(os.path.realpath(findex))
name = rpath.split("\\")[-1]
fi = open(findex, 'r')
flag = False
IV = None
tl = []
for line in fi.readlines():
if line.startswith("#EXT-X-KEY"):
# 如果存在 IV 則提取;
if line.split(",")[-1].startswith("IV="):
IV = line.split(",")[-1][5:]
IV = bytes.fromhex(IV)
if line.startswith("#EXTINF"):
flag = not flag
continue
if flag:
tmp = line.strip().split("/")[-1]
tmp = os.path.join(rpath, tmp)
tl.append(tmp)
flag = not flag
fi.close()
fk = open(os.path.join(rpath, "key"), 'rb')
key = fk.read()
fk.close()
return name, tl, key, IV
def aes_decode(data, key, IV):
# 如果沒有指定 IV 值,則直接使用 key 值
if not IV:
IV = key
cryptor = AES.new(key, AES.MODE_CBC, IV)
plain_text = cryptor.decrypt(data)
return plain_text
def main():
fp = os.listdir()
used = [s[:-4] for s in os.listdir("./result/")]
for ind in fp:
if not ind.isdigit():
continue
if ind in used:
continue
try:
name, fl, key, IV = fileList(os.path.join(ind, "index.m3u8"))
except:
print("-"*30)
print("[-] Errot! file: ", ind)
print("-"*30)
continue
print("[*] Begin process file: ", name)
start = time.time()
f = open(os.path