如何入門音視屏

概述

保存視頻的每一幀,每一個像素沒要必要,而且也是不現實的,因爲這個數據量太大了,以至於沒辦法存儲和傳輸,比如說,一個視頻大小是 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 制定了第一個實用的視頻編碼標準 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 的。這個系列編碼標準的最大優勢是它是開放的,免版權稅的。

術語

多媒體容器格式(封裝格式)

一個多媒體文件或者多媒體流可能包含多個視頻、音頻、字幕、同步信息,章節信息以及元數據等數據。也就是說通常看到的 .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。當某個圖像的 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 幀。

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 播放器所使用的核心編解碼庫,B 站(Bilibili)開源的 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() 函數:

1int avformat_open_input(AVFormatContext **ps, const char *filename,
2                        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() 函數打開輸入文件並儘可能地解析出文件格式:

1static int init_input(AVFormatContext *s, const char *filename,
2                      AVDictionary **options)

init_input() 中的關鍵代碼是:

1if ((ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options)) < 0)
2        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() 函數中的主要調用是這樣的:

1err = uc->prot->url_open2 ? uc->prot->url_open2(uc,
2                                                  uc->filename,
3                                                  uc->flags,
4                                                  options) :
5        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() 函數對輸入流進行探測分析:

1int 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() 函數就可以讀取完整的一幀數據了:

 1    do {
 2        if (!end_of_stream)
 3            if (av_read_frame(fmt_ctx, &pkt) < 0)
 4                end_of_stream = 1;
 5        if (end_of_stream) {
 6            pkt.data = NULL;
 7            pkt.size = 0;
 8        }
 9        if (pkt.stream_index == video_stream || end_of_stream) {
10            got_frame = 0;
11            if (pkt.pts == AV_NOPTS_VALUE)
12                pkt.pts = pkt.dts = i;
13            result = avcodec_decode_video2(ctx, fr, &got_frame, &pkt);
14            if (result < 0) {
15                av_log(NULL, AV_LOG_ERROR, "Error decoding frame\n");
16                return result;
17            }
18            if (got_frame) {
19                number_of_written_bytes = av_image_copy_to_buffer(byte_buffer, byte_buffer_size,
20                                        (const uint8_t* const *)fr->data, (const int*) fr->linesize,
21                                        ctx->pix_fmt, ctx->width, ctx->height, 1);
22                if (number_of_written_bytes < 0) {
23                    av_log(NULL, AV_LOG_ERROR, "Can't copy image to buffer\n");
24                    return number_of_written_bytes;
25                }
26                printf("%d, %10"PRId64", %10"PRId64", %8"PRId64", %8d, 0x%08lx\n", video_stream,
27                        fr->pts, fr->pkt_dts, av_frame_get_pkt_duration(fr),
28                        number_of_written_bytes, av_adler32_update(0, (const uint8_t*)byte_buffer, number_of_written_bytes));
29            }
30            av_packet_unref(&pkt);
31            av_init_packet(&pkt);
32        }
33        i++;
34    } while (!end_of_stream || got_frame);

編譯 ijkplayer

如果編譯過程中出現 linux-perf 相關文件未找到的錯誤可以在編譯腳本文件中添加下面這一行以禁用相關調試功能:

1export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-linux-perf"

如果想支持 webm 格式視頻的播放需要修改編譯腳本,添加 decoder,demuxer,parser 對相關格式的支持:

 1export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=opus"
 2export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=vp6"
 3export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=vp6a"
 4export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=vp8_cuvid"
 5export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=vp8_mediacodec"
 6export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=vp8_qsv"
 7export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=vorbis"
 8export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=flac"
 9export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=theora"
10export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=zlib"
11
12export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-demuxer=matroska"
13export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-demuxer=ogg"
14
15export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-parser=vp8"
16export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-parser=vp9"
17export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-parser=vorbis"
18export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-parser=opus"

如果想支持分段視頻(ffconcat 協議),首先需要修改編譯腳本以支持拼接協議:

1export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-protocol=concat"
2export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-demuxer=concat"

然後在 Java 層將 ffconcat 協議加入白名單並允許訪問不安全的路徑:

1ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "safe", 0);
2ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "protocol_whitelist", "ffconcat,file,http,https");
3ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "protocol_whitelist", "concat,http,tcp,https,tls,file");

ijkplayer k0.8.8 版本, 支持常見格式的 lite 版本,支持 HTTPS 協議的 .so 文件的編譯命令如下:

 1git clone https://github.com/Bilibili/ijkplayer.git ijkplayer-android
 2cd ijkplayer-android
 3git checkout -B latest k0.8.8
 4cd config
 5rm module.sh
 6ln -s module-lite.sh module.sh
 7cd ..
 8./init-android.sh
 9./init-android-openssl.sh
10cd android/contrib
11./compile-openssl.sh clean
12./compile-openssl.sh all
13./compile-ffmpeg.sh clean
14./compile-ffmpeg.sh all
15cd ..
16./compile-ijk.sh clean
17./compile-ijk.sh all

也可以簡化成一個命令:

1git 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 文件目錄位於如下目錄:

1ijkplayer-android/android/ijkplayer/ijkplayer-armv7a/src/main/libs/armeabi-v7a/libijkffmpeg.so

參考

MPEG-1 Wiki H.261 Wiki FFmpeg Wiki FFmpeg ijkplayer

作者:薛定貓的諤 鏈接:https://juejin.im/post/5b9a0c796fb9a05d0f16c665


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章