音視訊技術基礎
儲存視訊的每一幀,每一個畫素沒要必要,而且也是不現實的,因為這個資料量太大了,以至於沒辦法儲存和傳輸,比如說,一個視訊大小是 1280×720 畫素,一個畫素佔 12 個位元位,每秒 30 幀,那麼一分鐘這樣的視訊就要佔 1280×720×12×30×60/8/1024/1024=2.3G 的空間,所以視訊資料肯定要進行壓縮儲存和傳輸的。
而可以壓縮的冗餘資料有很多,從空間上來說,一幀影象中的畫素之間並不是毫無關係的,相鄰畫素有很強的相關性,可以利用這些相關性抽象地儲存。同樣在時間上,相鄰的視訊幀之間內容相似,也可以壓縮。每個畫素值出現的概率不同,從編碼上也可以壓縮。人類視覺系統(HVS)對高頻資訊不敏感,所以可以丟棄高頻資訊,只編碼低頻資訊。對高對比度更敏感,可以提高邊緣資訊的主觀質量。對亮度資訊比色度資訊更敏感,可以降低色度的解析度。對運動的資訊更敏感,可以對感興趣區域(ROI)進行特殊處理。
視訊資料壓縮和傳輸的實現與最終將這些資料還原成視訊播放出來的實現是緊密相關的,也就是說視訊資訊的壓縮和解壓縮需要一個統一標準,即音視訊編碼標準。
視訊編碼
制定音視訊編碼標準的有兩個組織機構,一個是國際電聯下屬的機構 ITU-T(ITU Telecommunication Standardization Sector),一個是國際標準化組織 ISO 和國際電工委員會 IEC 下屬的 MPEG(Moving Picture Experts Group) 專家組。
1988 年,ITU-T 制定了第一個實用的視訊編碼標準ofollow,noindex">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 和 開放媒體聯盟 Alliance for Open Media(AOMedia)開發的 AV1(AOMedia Video 1)都是基於 VP8 的。這個系列編碼標準的最大優勢是它是開放的,免版權稅的。
術語
多媒體容器格式(封裝格式)
一個多媒體檔案或者多媒體流可能包含多個視訊、音訊、字幕、同步資訊,章節資訊以及元資料等資料。也就是說通常看到的 .mp4 、.avi、.rmvb 等檔案中的 MP4、AVI 其實是一種容器格式(container formats),用來封裝這些資料,而不是視訊編碼。
muxer 和 demuxer
muxer 就是用來封裝多媒體容器格式的封裝器,比如把一個 rmvb 視訊檔案,mp3 音訊檔案以及 srt 字幕檔案,封裝成為一個新的 mp4 檔案。而 demuxer 就是解封裝器,可以將容器格式分解成視訊流、音訊流、附加資料等資訊。
Codec
編解碼器,是編碼器(Encoder)和 解碼器(Decoder)的統稱。
I 幀
Intra-frame,也被稱為 I-pictures 或 keyframes,也就是說俗稱的關鍵幀,是指不依賴於其他任何幀進行渲染的視訊幀,簡單呈現一個固定影象。兩個關鍵幀之間的視訊幀是可以預測計算出來的,但兩個 I 幀之間的幀數不可能特別大,因為解碼的複雜度,解碼器緩衝區大小,資料錯誤後的恢復時間,搜尋能力以及在硬體解碼器中最常見的低精度實現中 IDCT 錯誤的累積,限制了 I 幀之間的最大幀數。
P 幀
Predicted-frame,也被稱為向前預測幀或幀間幀,僅儲存與緊鄰它的前一個幀(I 幀或 P 幀,這個參考幀也稱為錨幀)的影象差異。使用幀的每個巨集塊上的運動向量計算 P 幀與其錨幀之間的差異,這種運動向量資料將嵌入 P 幀中以供解碼器使用。除了任何前向預測的塊之外,P 幀還可以包含任意數量的幀內編碼塊。如果視訊從一幀到下一幀(例如剪輯)急劇變化,則將其編碼為 I 幀會更有效。如果 P 幀丟失,視訊畫面可能會出現花屏或者馬賽克的現象。
B 幀
Bidirectional-frame,代表雙向幀,也被稱為向後預測幀或 B-pictures。 B 幀與 P 幀非常相似,B 幀可以使用前一幀和後一幀(即兩個錨幀)進行預測。因此,在可以解碼和顯示 B 幀之前,播放器必須首先在 B 幀之後順序解碼下一個 I 或 P 錨幀。這意味著解碼 B 幀需要更大的資料緩衝器,並導致解碼和編碼期間的延遲增加。這還需要容器/系統流中的解碼時間戳(DTS)特徵。因此,B 幀長期以來一直備受爭議,它們通常在視訊中被避免,有時硬體解碼器不能完全支援它們。不存在從 B 幀 預測的幀的,因此,可以在需要時插入非常低位元率的 B 幀,以幫助控制位元率。如果這是用 P 幀完成的,則可以從中預測未來的 P 幀,並且會降低整個序列的質量。除了向後預測或雙向預測的塊之外,B幀還可以包含任意數量的幀內編碼塊和前向預測塊。
NAL 和 VCL
網路抽象層 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。當某個影象的片的 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 幀。
DTS 和 PTS
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
FFMPEG 概述
FFMPEG 專案是在 2000 年由法國的程式員 Fabrice Bellard 發起的,名字是受到 MPEG 專家組的啟發,前面的“FF”是“fast forward”快進的意思。FFMPEG 是一個可以錄製音視訊,轉碼音視訊的格式,將音視訊轉成媒體流的完整的、跨平臺的解決方案。它是一個自由的軟體專案,任何人都可以免費使用和修改,只要遵循 GPL 或者 LGPL 協議引用或公開原始碼就行。它中的編解碼庫也是 VLC 播放器所使用的核心編解碼庫,ijkplayer 、MPlayer 等播放器也都是基於 FFMPEG 開發的。
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);
編譯 ijkplayer
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
也可以簡化成一個命令:
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
如果編譯過程中出現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"