保存視頻的每一幀,每個像素沒要必要,並且也是不現實的,由於這個數據量太大了,以致於沒辦法存儲和傳輸,好比說,一個視頻大小是 1280×720 像素,一個像素佔 12 個比特位,每秒 30 幀,那麼一分鐘這樣的視頻就要佔 1280×720×12×30×60/8/1024/1024=2.3GB 的空間,因此視頻數據確定要進行壓縮存儲和傳輸的。
而能夠壓縮的冗餘數據有不少,從空間上來講,一幀圖像中的像素之間並非毫無關係的,相鄰像素有很強的相關性,能夠利用這些相關性抽象地存儲。一樣在時間上,相鄰的視頻幀之間內容類似,也能夠壓縮。每一個像素值出現的機率不一樣,從編碼上也能夠壓縮。人類視覺系統(HVS)對高頻信息不敏感,因此能夠丟棄高頻信息,只編碼低頻信息。對高對比度更敏感,能夠提升邊緣信息的主觀質量。對亮度信息比色度信息更敏感,能夠下降色度的解析度。對運動的信息更敏感,能夠對感興趣區域(ROI)進行特殊處理。
視頻數據壓縮和傳輸的實現與最終將這些數據還原成視頻播放出來的實現是緊密相關的,也就是說視頻信息的壓縮和解壓縮須要一個統一標準,即音視頻編碼標準。html
制定音視頻編碼標準的有兩個組織機構,一個是國際電聯下屬的機構 ITU-T(ITU Telecommunication Standardization Sector),一個是國際標準化組織 ISO 和國際電工委員會 IEC 下屬的 MPEG(Moving Picture Experts Group) 專家組。
1988 年,ITU-T 制定了第一個實用的視頻編碼標準 H.261,這也是第一個 H.26x 家族的視頻編碼標準,以後的一些視頻編碼標準大多都是以此爲基礎的。它的的基本處理單元稱爲宏塊,H.261 是宏塊概念出現的第一個標準。每一個宏塊由 16×16 陣列的亮度樣本和兩個對應的 8×8 色度樣本陣列組成,使用 4:2:0 採樣和 YCbCr 色彩空間。編碼算法使用運動補償的圖片間預測和空間變換編碼的混合,涉及標量量化,Z 字形掃描和熵編碼。
1993 年,ISO/IEC 制定了有損壓縮標準 MPEG-1,其中最著名的部分是它引入的 MP3 音頻格式。
2003 年,ITU-T 和 MPEG 共同組成的 JVT(Joint Video Team)聯合視頻小組開發了優秀且廣爲流行的 H.264 標準,該標準既是 ITU-T 的 H.264 標準,也是 MPEG-4 的第十部分(第十部分也叫 AVC(Advanced Video Coding)),因此 H.264/AVC, AVC/H.264, H.264/MPEG-4 AVC, MPEG-4/H.264 AVC 都是指 H.264。而以後的 HEVC(High Efficiency Video Coding)視頻壓縮標準既是指 H.265 也是指 MPEG-H 第二部分。
2003 年,微軟基於 WMV9(Windows Media Video 9)格式開發了視頻編碼標準 VC-1。
2008 年,Google 基於 VP7 開源了 VP8 視頻壓縮格式。 VP8 能夠與 Vorbis 和 Opus 音頻一塊兒多路複用到基於 Matroska 的容器格式 WebM 中。圖像格式 WebP 基於 VP8 的幀內編碼。以後的 VP9 和 AOMedia(Alliance for Open Media)開發的 AV1(AOMedia Video 1)都是基於 VP8 的。這個系列編碼標準的最大優點是它是開放的,免版權稅的。java
一個多媒體文件或者多媒體流可能包含多個視頻、音頻、字幕、同步信息,章節信息以及元數據等數據。也就是說咱們一般看到的 .mp4 、.avi、.rmvb 等文件中的 MP四、AVI 實際上是一種容器格式(container formats),用來封裝這些數據,而不是視頻的編碼格式。linux
muxer 就是用來封裝多媒體容器格式的封裝器,好比把一個 rmvb 視頻文件,mp3 音頻文件以及 srt 字幕文件,封裝成爲一個新的 mp4 文件。而 demuxer 就是解封裝器,能夠將容器格式分解成視頻流、音頻流、附加數據等信息。android
編解碼器,是編碼器(Encoder)和 解碼器(Decoder)的統稱。git
Intra-frame,也被稱爲 I-pictures 或 keyframes,也就是說俗稱的關鍵幀,是指不依賴於其餘任何幀進行渲染的視頻幀,簡單呈現一個固定圖像。兩個關鍵幀之間的視頻幀是能夠預測計算出來的,但兩個 I 幀之間的幀數不可能特別大,由於解碼的複雜度,解碼器緩衝區大小,數據錯誤後的恢復時間,搜索能力以及在硬件解碼器中最多見的低精度實現中 IDCT 錯誤的累積,限制了 I 幀之間的最大幀數。程序員
Predicted-frame,也被稱爲向前預測幀或幀間幀,僅存儲與緊鄰它的前一個幀(I 幀或 P 幀,這個參考幀也稱爲錨幀)的圖像差別。使用幀的每一個宏塊上的運動矢量計算 P 幀與其錨幀之間的差別,這種運動矢量數據將嵌入 P 幀中以供解碼器使用。除了任何前向預測的塊以外,P 幀還能夠包含任意數量的幀內編碼塊。若是視頻從一幀到下一幀(例如剪輯)急劇變化,則將其編碼爲 I 幀會更有效。若是 P 幀丟失,視頻畫面可能會出現花屏或者馬賽克的現象。github
Bidirectional-frame,表明雙向幀,也被稱爲向後預測幀或 B-pictures。 B 幀與 P 幀很是類似,B 幀可使用前一幀和後一幀(即兩個錨幀)進行預測。所以,在能夠解碼和顯示 B 幀以前,播放器必須首先在 B 幀以後順序解碼下一個 I 或 P 錨幀。這意味着解碼 B 幀須要更大的數據緩衝器,並致使解碼和編碼期間的延遲增長。這還須要容器/系統流中的解碼時間戳(DTS)特徵。所以,B 幀長期以來一直備受爭議,它們一般在視頻中被避免,有時硬件解碼器不能徹底支持它們。不存在從 B 幀 預測的幀的,所以,能夠在須要時插入很是低比特率的 B 幀,以幫助控制比特率。若是這是用 P 幀完成的,則能夠從中預測將來的 P 幀,而且會下降整個序列的質量。除了向後預測或雙向預測的塊以外,B幀還能夠包含任意數量的幀內編碼塊和前向預測塊。web
網絡抽象層 NAL(Network Abstraction Layer)和 視頻編碼層 VCL(Video Coding Layer)是 H.264/AVC 和 HEVC 標準的一部分,NAL 的主要目的是對訪問「會話」(視頻通話)和「非會話」(存儲、傳播、轉成媒體流)應用的網絡友好的視頻表示一個規定。NAL 用來格式化 VCL 的視頻表示,並以適當的方式爲經過各類傳輸層和存儲介質進行的傳輸提供頭信息。也就是說 NAL 有助於將 VCL 數據映射到傳輸層。
NALU(NAL units)是已編碼的視頻數據用來存儲和傳輸的基本單元,NAL 單元的前一個(H.264/AVC)或兩個(HEVC)字節是 Header 字節,用來標明該 NAL 單元中數據的類型。其它字節是有效載荷。
NAL 單元分爲 VCL 和非 VCL 的 NAL 單元。VCL NAL 單元包含表示視頻圖像中樣本值的數據,非 VCL NAL 單元包含任何相關的附加信息,例如參數集 parameter sets(可應用於大量 VCL NAL 單元的重要 header 數據)和補充加強信息 SEI(Supplemental enhancement information)(定時信息和其餘能夠加強解碼視頻信號可用性的補充數據,但對於解碼視頻圖像中的樣本的值不是必需的)。
參數集分爲兩種類型: SPS(sequence parameter sets)和 PPS(picture parameter sets)。SPS 應用於一系列連續的已編碼的視頻圖像(即已編碼視頻序列),PPS 應用於已編碼視頻序列中一個或多個單獨圖像的解碼。也就是說 SPS 和 PPS 將不頻繁改變信息的傳輸和視頻圖像中樣本值編碼表示的傳輸分離開來。每一個 VCL NAL 單元包含一個指向相關 PPS 內容的標識符,而每一個 PPS 都包含一個指向相關 SPS 內容的標識符。所以僅僅經過少許數據(標識符)就能夠引用大量的信息(參數集)而無需在每一個 VCL NAL 單元中重複該信息了。SPS 和 PPS 能夠在它們要應用的 VCL NAL 單元以前發送,而且能夠重複發送以提高針對數據丟失的頑健性。
NAL Header 字節中的 nal_ref_idc 用於表示當前 NALU 的重要性,值越大,越重要,解碼器在解碼處理不過來的時候,能夠丟掉重要性爲 0 的 NALU。SPS/PPS 時,nal_ref_idc 不可爲 0。當某個圖像的 slice 的 nal_ref_id 等於 0 時,該圖像的全部片均應等 0。nal_unit_type 表示 NALU 的類型,7 表示這個 NALU 是 SPS,8 表示這個 NALU 是 PPS。5 表示這個 NALU 是 IDR(instantaneous decoding refresh,即 I 幀) 的 slice,1 表示這個 NALU 所在的幀是 P 幀。算法
PS(Program Streams)指將多個打包的基本碼流 PES (一般是一個音頻 PES 和一個視頻 PES)組合成的單個流,以確保同時傳送並保持同步,PS 也被稱爲多路傳輸(multiplex)或容器格式(container format)。
PTS(Presentation time stamps): PS 中的 PTS 用來校訂音頻和視頻 SCR(system clock reference)值之間的不可避免的差別(時基校訂),如 PS 頭中的 90 kHz PTS 值告訴解碼器哪些視頻 SCR 值與哪些音頻 SCR 值匹配。PTS 決定了什麼時候顯示 MPEG program 的一部分,而且解碼器還使用它來肯定什麼時候能夠從緩衝器中丟棄數據。解碼器將延遲視頻或音頻中的一個,直到另外一個的相應片斷到達而且能夠被解碼。
DTS(Decoding Time Stamps): 對於視頻流中的 B 幀,必須對相鄰幀進行無序編碼和解碼(從新排序的幀)。DTS 與 PTS 很是類似,但它不只僅處理順序幀,而是包含適當的時間戳,在它的錨幀(P 幀 或 I 幀)以前,告訴解碼器什麼時候解碼並顯示下一個 B 幀。若是視頻中沒有B幀,那麼 PTS 和 DTS 值是相同的。安全
FFMPEG 項目是在 2000 年由法國著名程序員 Fabrice Bellard 發起的,名字是受到 MPEG 專家組的啓發,前面的 「FF」 是 「fast forward」 快進的意思。FFMPEG 是一個能夠錄製音視頻,轉碼音視頻的格式,將音視頻轉成媒體流的完整的、跨平臺的 解決方案。它是一個自由的軟件項目,任何人均可以避免費使用和修改,只要遵循 GPL 或者 LGPL 協議引用或公開源碼就行。它中的編解碼庫也是 VLC 播放器所使用的核心編解碼庫,B 站(Bilibili)開源的 ijkplayer 、著名的 MPlayer 等基本全部主流播放器也都是基於 FFMPEG 開發的。
libavcodec/allcodecs.c
文件中的 avcodec_register_all()
函數用來註冊全部的編解碼器(包括硬件加速、視頻、音頻、PCM、DPCM、ADPCM、字幕、文本、外部庫、解析器)。
libavformat/allformats.c
文件中的 av_register_all()
函數中調用了 avcodec_register_all()
註冊全部的編解碼器並註冊了全部 muxer 和 demuxer。
所以使用 FFMPEG 通常都要先調用 av_register_all()
。
要讀取一個媒體文件,可使用 libavformat/utils.c
文件中的 avformat_open_input()
函數:
int avformat_open_input(AVFormatContext **ps, const char *filename, AVInputFormat *fmt, AVDictionary **options) 複製代碼
ps
包含了媒體相關的基本全部數據,隨後函數中調用的 libavformat/options.c
文件中的 avformat_alloc_context()
函數會爲它分配空間,而 avformat_alloc_context()
中會調用 avformat_get_context_defaults()
給 s->io_open
設置默認值 io_open_default()
函數。
filename
是想要讀取的媒體文件的路徑表示,能夠是本地或者網絡的。
fmt
是自定義的讀取格式,能夠爲 NULL
也能夠提早經過 av_find_input_format()
函數獲取。
options
是特殊操做參數,如設置 timeout
參數的值。
avformat_open_input()
中會調用 init_input()
函數打開輸入文件並儘量地解析出文件格式:
static int init_input(AVFormatContext *s, const char *filename, AVDictionary **options) 複製代碼
init_input()
中的關鍵代碼是:
if ((ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options)) < 0)
return ret;
複製代碼
而前面說的 s->io_open
默認指向的 libavformat/option.c
文件中的 io_open_default()
函數會調用 libavformat/aviobuf.c
文件中的 ffio_open_whitelist()
函數。
ffio_open_whitelist()
函數會先調用 libavformat/avio.c
文件中的 ffurl_open_whitelist()
函數初始化 URLContext
,再調用 libavformat/aviobuf.c
文件中的 ffio_fdopen()
函數根據 URLContext
的真正類型(如 HTTPContext
)初始化 AVIOContext
,這個 AVIOContext
就是常見的 s->pb
,也就是說從這時開始 pb
已經被初始化了。
ffurl_open_whitelist()
函數中會先調用 ffurl_alloc()
函數找到協議真正類型並根據類型爲 URLContext
分配空間,再調用 ffurl_connect()
函數打開媒體文件。
ffurl_connect()
函數中的主要調用是這樣的:
err =
uc->prot->url_open2 ? uc->prot->url_open2(uc,
uc->filename,
uc->flags,
options) :
uc->prot->url_open(uc, uc->filename, uc->flags);
複製代碼
而位於 libavformat/http.c
文件中的 HTTP 協議 ff_http_protocol
的 url_open2
指向了 http_open()
函數,http_open()
中經過 HTTPContext
中的 AVApplicationContext
能夠跟上層進行通信,好比告訴上層正在進行 HTTP 請求,但主要調用的 http_open_cnx()
函數調用了 http_open_cnx_internal()
。
http_open_cnx_internal()
中先是對視頻 URL 進行分析,好比若是使用了代理那麼還要從新組裝 URL 以免將一些信息暴露給代理服務器,若是是 HTTPS 那麼底層協議就是 TLS 不然底層協議就是 TCP,而後調用 ffurl_open_whitelist()
進行底層協議的處理(如 DNS 解析,TCP 握手創建 Socket 鏈接)。而後調用 http_connect()
函數進行 HTTP 請求,固然請求前要給 Header 設置默認值而且添加用戶自定義的 Header,而後調用 libavformat/avio.c
文件中的 ffurl_write()
函數發送請求數據,它調用底層協議的 url_write
,而位於 libavformat/tcp.c
文件中的 TCP 協議 ff_tcp_protocol
的 url_write
指向了 tcp_write()
函數,tcp_write()
主要是調用系統函數 send()
發送數據(tcp_read
調用系統函數 recv()
)。最後,在發送完數據後會調用 http_read_header()
函數讀取響應報文的 Header,而 http_read_header()
中有個死循環,就是不停地 http_get_line()
和 process_line()
直到全部 Header 數據處理完畢,http_get_line()
內部其實也是調用了 ffurl_read()
(跟 ffurl_write()
邏輯相似)。
至此,若是 avformat_open_input()
返回了大於等於零的數,就算是第一次拿到了媒體文件的數據,播放器就能夠向上層發一個 FFP_MSG_OPEN_INPUT
的消息表示成功打開了輸入流。
打開輸入流並必定能精確地知道媒體流實際的時長、幀率等信息,通常狀況下還須要調用 libavformat/utils.c
文件中的 avformat_find_stream_info()
函數對輸入流進行探測分析:
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options) 複製代碼
因爲讀取一部分媒體數據進行分析的過程仍是很是耗時的,因此須要一個時間限制,這個時間限制不能過短以免成功率過低。max_analyze_duration
若是不指定那麼默認是 5 * AV_TIME_BASE
(時間都是基於時基的,而時基 AV_TIME_BASE
是 1000000
),對於 mpeg
或 mpegts
格式的視頻流 max_stream_analyze_duration = 90 * AV_TIME_BASE
。
對於媒體中的全部流(包括視頻流、音頻流、字幕流),先根據以前的 codec_id
調用 find_probe_decoder()
函數尋找合適的解碼器,再調用 libavcodec/utils.c
文件中的 avcodec_open2()
函數打開解碼器,再調用 read_frame_internal()
函數讀取一個完整的 AVPacket
,再調用 try_decode_frame()
函數嘗試解碼 packet。
通常媒體流中都會包括 AVMEDIA_TYPE_VIDEO
、AVMEDIA_TYPE_AUDIO
和 AVMEDIA_TYPE_SUBTITLE
等媒體類型的流,能夠經過 libavformat/utils.c
文件中的 av_find_best_stream()
函數獲取他們的索引。
根據各個媒體流的索引就能夠打開各個媒體流了,首先調用 libavcodec/utils.c
文件中的 avcodec_find_decoder()
函數找到該媒體流的解碼器,而後調用 libavcodec/options.c
文件中的 avcodec_alloc_context3()
爲解碼器分配空間,而後調用 libavcodec/utils.c
文件中的 avcodec_parameters_to_context()
爲解碼器複製上下文參數,而後調用 libavcodec/utils.c
文件中的 avcodec_open2()
打開解碼器,而後調用 libavutil/frame.c
文件中的 av_frame_alloc()
爲 AVFrame
分配空間,而後調用 libavutil/imgutils.c
文件中的 av_image_get_buffer_size()
獲取須要的緩衝區大小併爲其分配空間,而後調用 libavcodec/avpacket.c
文件中的 av_init_packet()
對 AVPacket
進行初始化。
經過 libavformat/utils.c
文件中的 av_read_frame()
函數就能夠讀取完整的一幀數據了:
do {
if (!end_of_stream)
if (av_read_frame(fmt_ctx, &pkt) < 0)
end_of_stream = 1;
if (end_of_stream) {
pkt.data = NULL;
pkt.size = 0;
}
if (pkt.stream_index == video_stream || end_of_stream) {
got_frame = 0;
if (pkt.pts == AV_NOPTS_VALUE)
pkt.pts = pkt.dts = i;
result = avcodec_decode_video2(ctx, fr, &got_frame, &pkt);
if (result < 0) {
av_log(NULL, AV_LOG_ERROR, "Error decoding frame\n");
return result;
}
if (got_frame) {
number_of_written_bytes = av_image_copy_to_buffer(byte_buffer, byte_buffer_size,
(const uint8_t* const *)fr->data, (const int*) fr->linesize,
ctx->pix_fmt, ctx->width, ctx->height, 1);
if (number_of_written_bytes < 0) {
av_log(NULL, AV_LOG_ERROR, "Can't copy image to buffer\n");
return number_of_written_bytes;
}
printf("%d, %10"PRId64", %10"PRId64", %8"PRId64", %8d, 0x%08lx\n", video_stream,
fr->pts, fr->pkt_dts, av_frame_get_pkt_duration(fr),
number_of_written_bytes, av_adler32_update(0, (const uint8_t*)byte_buffer, number_of_written_bytes));
}
av_packet_unref(&pkt);
av_init_packet(&pkt);
}
i++;
} while (!end_of_stream || got_frame);
複製代碼
若是編譯過程當中出現 linux-perf
相關文件未找到的錯誤能夠在編譯腳本文件中添加下面這一行以禁用相關調試功能:
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-linux-perf"
複製代碼
若是想支持 webm 格式視頻的播放須要修改編譯腳本,添加 decoder,demuxer,parser 對相關格式的支持:
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=opus"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=vp6"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=vp6a"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=vp8_cuvid"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=vp8_mediacodec"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=vp8_qsv"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=vorbis"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=flac"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=theora"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=zlib"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-demuxer=matroska"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-demuxer=ogg"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-parser=vp8"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-parser=vp9"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-parser=vorbis"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-parser=opus"
複製代碼
若是想支持分段視頻(ffconcat
協議),首先須要修改編譯腳本以支持拼接協議:
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-protocol=concat"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-demuxer=concat"
複製代碼
而後在 Java 層將 ffconcat
協議加入白名單並容許訪問不安全的路徑:
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "safe", 0);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "protocol_whitelist", "ffconcat,file,http,https");
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "protocol_whitelist", "concat,http,tcp,https,tls,file");
複製代碼
ijkplayer k0.8.8 版本, 支持常見格式的 lite 版本,支持 HTTPS 協議的 .so 文件的編譯命令以下:
git clone https://github.com/Bilibili/ijkplayer.git ijkplayer-android
cd ijkplayer-android
git checkout -B latest k0.8.8
cd config
rm module.sh
ln -s module-lite.sh module.sh
cd ..
./init-android.sh
./init-android-openssl.sh
cd android/contrib
./compile-openssl.sh clean
./compile-openssl.sh all
./compile-ffmpeg.sh clean
./compile-ffmpeg.sh all
cd ..
./compile-ijk.sh clean
./compile-ijk.sh all
複製代碼
也能夠簡化成一個命令:
git clone https://github.com/Bilibili/ijkplayer.git ijkplayer-android && cd ijkplayer-android && git checkout -B latest k0.8.8 && cd config && rm module.sh && ln -s module-lite.sh module.sh && cd .. && ./init-android.sh && ./init-android-openssl.sh && cd android/contrib && ./compile-openssl.sh clean && ./compile-openssl.sh all && ./compile-ffmpeg.sh clean && ./compile-ffmpeg.sh all && cd .. && ./compile-ijk.sh clean && ./compile-ijk.sh all
複製代碼
生成的 libijkffmpeg.so
,libijkplayer.so
,libijksdl.so
文件目錄位於以下目錄:
ijkplayer-android/android/ijkplayer/ijkplayer-armv7a/src/main/libs/armeabi-v7a/libijkffmpeg.so
複製代碼