目錄
- MP4的“問題”
- m3u8是什么
- m3u8的好處
- 源碼分析
- 擴展思考:mp4能不能像m3u8一樣進(jìn)行分片緩存吶陆淀?
- 資料
- 收獲
一剿配、MP4的“問題”
我們上面兩篇邊緩存邊播放之AndroidVideoCache和邊緩存邊播放之緩存分片都針對MP4格式進(jìn)行緩存處理克胳,由于很多視頻都是mp4格式,所以市面上商用的或者開源的播放器和緩存項目都是只支持MP4. 但是mp4格式有兩個弊端(當(dāng)然也是有辦法進(jìn)行優(yōu)化的)
1.1 moov在mdat后影響秒開率
Mp4格式是一個個Box溜嗜,其中moov存儲的是metadata信息,mdat存儲具體音視頻數(shù)據(jù)信息哪工。如果無法解析出moov數(shù)據(jù)赡麦,是無法播放該mp4文件的矾利。而一般情況下ffmpeg生成moov是在mdat寫入完成之后的,即mdat會在moov的前面肌幽,用mediaParse來查看一個mp4視頻的結(jié)構(gòu)如下
這樣就影響用戶體驗(首幀加載時長過長)。
針對這種情況,通用的做法是在服務(wù)端做處理款票。通過ffmpeg命令吧moov移動到mdat前面摘完。
ffmpeg -i in.mp4 -movflags faststart out.mp4
再用mediaParse來查看一個mp4視頻的結(jié)構(gòu)如下
1.2 緩存分片的顆粒太大、文件空洞占用空間
上一篇我們通過文件空洞的方式進(jìn)行緩存分片,雖然可以實現(xiàn)按塊分片緩存,但是占用額外的空間(空洞也會在用)造成資源浪費。
那么有沒有其他的方式來進(jìn)行緩存分片吶祭往?下面我們就開始進(jìn)入今天的主題M3U8分片緩存
二浪读、什么是m3u8
m3u8 文件是 HTTP Live Streaming(縮寫為 HLS) 協(xié)議的部分內(nèi)容折联,而 HLS 是一個由蘋果公司提出的基于 HTTP 的流媒體網(wǎng)絡(luò)傳輸協(xié)議脸狸。
HLS 是新一代流媒體傳輸協(xié)議,其基本實現(xiàn)原理為將一個大的媒體文件進(jìn)行分片官辽,將該分片文件資源路徑記錄于 m3u8 文件(即 playlist)內(nèi)蛹磺,其中附帶一些額外描述(比如該資源的多帶寬信息···)用于提供給客戶端⊥停客戶端依據(jù)該 m3u8 文件即可獲取對應(yīng)的媒體資源萤捆,進(jìn)行播放。
m3u8 文件格式詳解
把mp4轉(zhuǎn)為ts m3u8
//如果視頻是h264
ffmpeg -y -i 11.mp4 -vcodec copy -vbsf h264_mp4toannexb out.ts
//如果視頻是h265
ffmpeg -y -i 11.mp4 -vcodec copy -vbsf hevc_mp4toannexb out.ts
將ts切成小的ts片
ffmpeg -i out.ts -c copy -map 0 -f segment -segment_list ts/index.m3u8 -segment_time 15 ts/out-%04d.ts
//-f segment:切片
//-segment_list :輸出切片的m3u8
//-segment_time:每個切片的時間(單位秒)
可以看到包含了一個m3u8文件和多個ts文件俗批,其中M3U8是描述文件俗或,ts是媒體文件。
我們先來看下M3U8文件
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:16 --> 共16個ts片
#EXTINF:15.520000, --> 該片的時長
out-0000.ts --> 該片的名稱
#EXTINF:14.360000,
out-0001.ts
#EXTINF:15.720000,
out-0002.ts
#EXTINF:14.720000,
out-0003.ts
#EXTINF:14.440000,
out-0004.ts
#EXTINF:15.280000,
out-0005.ts
#EXTINF:15.640000,
out-0006.ts
#EXTINF:14.560000,
out-0007.ts
#EXTINF:15.040000,
out-0008.ts
#EXTINF:15.360000,
out-0009.ts
#EXTINF:14.640000,
out-0010.ts
#EXTINF:14.200000,
out-0011.ts
#EXTINF:15.160000,
out-0012.ts
#EXTINF:14.760000,
out-0013.ts
#EXTINF:15.640000,
out-0014.ts
#EXTINF:14.720000,
out-0015.ts
#EXTINF:9.960000,
out-0016.ts
#EXT-X-ENDLIST
m3u8文件是一個播放列表(playlist)索引岁忘,記錄了一系列媒體片段資源辛慰,順序播放該片段資源,即可完整展示多媒體資源干像。
ts是視頻流文件
// ffprobe /Users/yabin/Desktop/tmp/ts/out-0001.ts
Duration: 00:00:14.36, start: 16.960000, bitrate: 351 kb/s
Program 1
Metadata:
service_name : Service01
service_provider: FFmpeg
Stream #0:0[0x100]: Video: hevc (Main) (HEVC / 0x43564548), yuv420p(tv), 590x1280, 25 fps, 25 tbr, 90k tbn, 25 tbc
ts文件是一種視頻切片文件帅腌,可以直接播放
對于點播來說,客戶端只需按順序下載上述片段資源麻汰,依次進(jìn)行播放即可狞膘。而對于直播來說,客戶端需要 定時重新請求 該 m3u8 文件,看下是否有新的片段數(shù)據(jù)需要進(jìn)行下載并播放
m3u8 文件格式詳解
三、m3u8的好處
通過上面小節(jié)父能,我們知道m(xù)3u8是一種一個協(xié)議盼理,里面存儲的是視頻塊的索引文件。那么它適用于什么場景吶智亮?使用mp4還是m3u8+ts吶忆某?
m3u8 采用切塊技術(shù),下載的播放文件 就可以少很多阔蛉,只有當(dāng)前播放的部分弃舒,可以更好的進(jìn)行帶寬控制。當(dāng)然使用MP4方式下載時也是可以進(jìn)行控制帶寬。
對于短視頻來說聋呢,由于文件比較小苗踪,直接使用mp4 從下載和播放速度以及流量上都沒什么問題。
對于長視頻而言削锰, 由于moov比較大通铲,頭部解析比較耗時,緩存是以整個文件為單位的器贩,而m3u8切片的方式保證了可以單獨下載單獨緩存颅夺,提高了復(fù)用率。在使用P2P技術(shù)方案時可以直接作為種源蛹稍。
另外m3u8還可以 根據(jù)用戶的網(wǎng)絡(luò)帶寬情況吧黄,自動為客戶端匹配一個合適的碼率文件進(jìn)行播放,從而保證視頻的流暢度唆姐。
四拗慨、源碼分析
我們接續(xù)看下開源項目 JeffVideoCache 的實現(xiàn)。
主流程和邊緩存邊播放之緩存分片-物理文件空洞方案基本一致厦酬。主要的差異點在玉m3u8索引文件的解析胆描,以及每個片單獨下載邏輯。
4.1 M3U8結(jié)構(gòu)體定義
首先定義兩個結(jié)構(gòu)體M3U8
和M3U8Seg
仗阅,其中結(jié)構(gòu)體M3U8對應(yīng)的事索引文件昌讲,而M3U8Seg
對應(yīng)的是M3U8文件中TS文件的結(jié)構(gòu)
public class M3U8 {
private String mUrl; //M3U8的url
private float mTargetDuration; //指定的duration
private int mSequence = 0; //序列起始值
private int mVersion = 3; //版本號
private boolean mIsLive; //是否是直播
private List<M3U8Seg> mSegList; //分片seg 列表
}
public class M3U8Seg {
private String mParentUrl; //分片的上級M3U8的url
private String mUrl; //分片的網(wǎng)絡(luò)url
private String mName; //分片的文件名
private float mDuration; //分片的時長
private int mSegIndex; //分片索引位置,起始索引為0
private long mFileSize; //分片文件大小
private long mContentLength; //分片文件的網(wǎng)絡(luò)請求的content-length
private boolean mHasDiscontinuity; //當(dāng)前分片文件前是否有Discontinuity
private boolean mHasKey; //分片文件是否加密
private String mMethod; //分片文件的加密方法
private String mKeyUrl; //分片文件的密鑰地址
private String mKeyIv; //密鑰IV
private int mRetryCount; //重試請求次數(shù)
private boolean mHasInitSegment; //分片前是否有#EXT-X-MAP
private String mInitSegmentUri; //MAP的url
private String mSegmentByteRange; //MAP的range
}
4.2 M3U8文件解析
根據(jù)m3u8的url從網(wǎng)絡(luò)請求獲取到對應(yīng)的索引文件减噪,然后根據(jù)m3u8協(xié)議進(jìn)行解析短绸,生成對應(yīng)的M3U8和M3U8Seg對象。
public static M3U8 parseNetworkM3U8Info(String parentUrl, String videoUrl, Map<String, String> headers, int retryCount) throws IOException {
InputStreamReader inputStreamReader = null;
BufferedReader bufferedReader = null;
try {
HttpURLConnection connection = HttpUtils.getConnection(videoUrl, headers);
int responseCode = connection.getResponseCode();
if (responseCode == HttpUtils.RESPONSE_503 && retryCount < HttpUtils.MAX_RETRY_COUNT) {
return parseNetworkM3U8Info(parentUrl, videoUrl, headers, retryCount + 1);
}
bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
M3U8 m3u8 = new M3U8(videoUrl);
int targetDuration = 0;
int version = 0;
int sequence = 0;
boolean hasDiscontinuity = false;
boolean hasEndList = false;
boolean hasMasterList = false;
boolean hasKey = false;
boolean hasInitSegment = false;
String method = null;
String keyIv = null;
String keyUrl = null;
String initSegmentUri = null;
String segmentByteRange = null;
float segDuration = 0;
int segIndex = 0;
String line;
while ((line = bufferedReader.readLine()) != null) {
line = line.trim();
if (TextUtils.isEmpty(line)) {
continue;
}
/**
* #EXTM3U
* #EXT-X-VERSION:3 -->Constants.TAG_VERSION
* #EXT-X-MEDIA-SEQUENCE:0 -->Constants.TAG_MEDIA_SEQUENCE
* #EXT-X-ALLOW-CACHE:YES
* #EXT-X-TARGETDURATION:16 -->Constants.TAG_TARGET_DURATION
* #EXTINF:15.520000, -->Constants.TAG_MEDIA_DURATION
* out-0000.ts
* #EXTINF:14.360000,
* out-0001.ts
* #EXT-X-ENDLIST --> Constants.TAG_ENDLIST
*/
if (line.startsWith(Constants.TAG_PREFIX)) {
if (line.startsWith(Constants.TAG_MEDIA_DURATION)) {
String ret = parseStringAttr(line, Constants.REGEX_MEDIA_DURATION);
if (!TextUtils.isEmpty(ret)) {
segDuration = Float.parseFloat(ret);
}
} else if (line.startsWith(Constants.TAG_TARGET_DURATION)) {
String ret = parseStringAttr(line, Constants.REGEX_TARGET_DURATION);
if (!TextUtils.isEmpty(ret)) {
targetDuration = Integer.parseInt(ret);
}
} else if (line.startsWith(Constants.TAG_VERSION)) {
String ret = parseStringAttr(line, Constants.REGEX_VERSION);
if (!TextUtils.isEmpty(ret)) {
version = Integer.parseInt(ret);
}
} else if (line.startsWith(Constants.TAG_MEDIA_SEQUENCE)) {
String ret = parseStringAttr(line, Constants.REGEX_MEDIA_SEQUENCE);
if (!TextUtils.isEmpty(ret)) {
sequence = Integer.parseInt(ret);
}
} else if (line.startsWith(Constants.TAG_STREAM_INF)) { //不一定有
hasMasterList = true;
} else if (line.startsWith(Constants.TAG_DISCONTINUITY)) { //不一定有
hasDiscontinuity = true;
} else if (line.startsWith(Constants.TAG_ENDLIST)) {
hasEndList = true;
} else if (line.startsWith(Constants.TAG_KEY)) { //不一定有
hasKey = true;
method = parseOptionalStringAttr(line, Constants.REGEX_METHOD);
String keyFormat = parseOptionalStringAttr(line, Constants.REGEX_KEYFORMAT);
if (!Constants.METHOD_NONE.equals(method)) {
keyIv = parseOptionalStringAttr(line, Constants.REGEX_IV);
if (Constants.KEYFORMAT_IDENTITY.equals(keyFormat) || keyFormat == null) {
if (Constants.METHOD_AES_128.equals(method)) {
// The segment is fully encrypted using an identity key.
String tempKeyUri = parseStringAttr(line, Constants.REGEX_URI);
if (tempKeyUri != null) {
keyUrl = UrlUtils.getM3U8MasterUrl(videoUrl, tempKeyUri);
}
} else {
// Do nothing. Samples are encrypted using an identity key,
// but this is not supported. Hopefully, a traditional DRM
// alternative is also provided.
}
} else {
// Do nothing.
}
}
} else if (line.startsWith(Constants.TAG_INIT_SEGMENT)) { //不一定有
String tempInitSegmentUri = parseStringAttr(line, Constants.REGEX_URI);
if (!TextUtils.isEmpty(tempInitSegmentUri)) {
hasInitSegment = true;
initSegmentUri = UrlUtils.getM3U8MasterUrl(videoUrl, tempInitSegmentUri);
segmentByteRange = parseOptionalStringAttr(line, Constants.REGEX_ATTR_BYTERANGE);
}
}
continue;
}
// It has '#EXT-X-STREAM-INF' tag;
if (hasMasterList) {
String tempUrl = UrlUtils.getM3U8MasterUrl(videoUrl, line);
return parseNetworkM3U8Info(parentUrl, tempUrl, headers, retryCount);
}
if (Math.abs(segDuration) < 0.001f) {
continue;
}
M3U8Seg seg = new M3U8Seg();
seg.setParentUrl(parentUrl);
String tempUrl = UrlUtils.getM3U8MasterUrl(videoUrl, line);
seg.setUrl(tempUrl);
seg.setSegIndex(segIndex);
seg.setDuration(segDuration);
seg.setHasDiscontinuity(hasDiscontinuity);
seg.setHasKey(hasKey);
if (hasKey) {
seg.setMethod(method);
seg.setKeyIv(keyIv);
seg.setKeyUrl(keyUrl);
}
if (hasInitSegment) {
seg.setInitSegmentInfo(initSegmentUri, segmentByteRange);
}
m3u8.addSeg(seg);
segIndex++;
segDuration = 0;
hasDiscontinuity = false;
hasKey = false;
hasInitSegment = false;
method = null;
keyUrl = null;
keyIv = null;
initSegmentUri = null;
segmentByteRange = null;
}
m3u8.setTargetDuration(targetDuration);
m3u8.setVersion(version);
m3u8.setSequence(sequence);
m3u8.setIsLive(!hasEndList);
return m3u8;
} catch (IOException e) {
throw e;
} finally {
ProxyCacheUtils.close(inputStreamReader);
ProxyCacheUtils.close(bufferedReader);
}
}
4.3 為了實現(xiàn)變下載邊播放也要通過本地代理的方式
需要把M3U8Seg中的鏈接給替換成本地代理的地址
接下來就是進(jìn)行網(wǎng)絡(luò)請求和MP4的方式查不了太多筹裕,我們就不繼續(xù)分析了醋闭。
五、擴展思考:mp4能不能像m3u8一樣進(jìn)行分片緩存吶朝卒?
對于長視頻证逻,由于歷史原因我們使用的也是mp4方式,這樣在首幀加載時長(由于moov過大)以及緩存切片(除了像上一篇講的物理文件空洞)抗斤、帶寬和流暢度控制(由于沒有像m3u8支持不同碼率的切換)存在一些可優(yōu)化點囚企。
對于首幀加載我們可以采用預(yù)加載的策略進(jìn)行優(yōu)化。
對于帶寬方面我們也可以根據(jù)碼率和下載進(jìn)度情況進(jìn)行控制瑞眼。
那么緩存切片上是否可以借鑒m3u8對一個物理文件進(jìn)行邏輯切片龙宏,然后針對單獨的邏輯切片(而不是物理文件空洞的方式)進(jìn)行單獨緩存吶? 歡迎交流
六伤疙、資料
七、收獲
通過本篇的學(xué)習(xí)時間
- 了解了MP4的“問題”(moov和mdat的順序影響解析速度黍特、長視頻緩存整個文件為單位緩存導(dǎo)致命中率和復(fù)用率不夠高)
- 了解M3U8是一種協(xié)議蛙讥,對視頻進(jìn)行ts切片,可以根據(jù)不同網(wǎng)絡(luò)切換不同切片的碼率衅澈、緩存的大小可以以更小可以的切片為單位等優(yōu)點
- 簡單分析了JeffVideoCache對M3U8的解析和緩存支持键菱。
感謝你的閱讀
下一篇我們開始多線程并發(fā)的學(xué)習(xí)實踐,歡迎關(guān)注公眾號“音視頻開發(fā)之旅”今布,一起學(xué)習(xí)成長经备。
歡迎交流