音視訊技術基礎

薛定貓的諤發表於2018-09-29

音視訊技術基礎

概述

儲存視訊的每一幀,每一個畫素沒要必要,而且也是不現實的,因為這個資料量太大了,以至於沒辦法儲存和傳輸,比如說,一個視訊大小是 1280×720 畫素,一個畫素佔 12 個位元位,每秒 30 幀,那麼一分鐘這樣的視訊就要佔 1280×720×12×30×60/8/1024/1024=2.3GB 的空間,所以視訊資料肯定要進行壓縮儲存和傳輸的。
而可以壓縮的冗餘資料有很多,從空間上來說,一幀影像中的畫素之間並不是毫無關係的,相鄰畫素有很強的相關性,可以利用這些相關性抽象地儲存。同樣在時間上,相鄰的視訊幀之間內容相似,也可以壓縮。每個畫素值出現的概率不同,從編碼上也可以壓縮。人類視覺系統(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() 函式:

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_protocolurl_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_protocolurl_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_BASE1000000),對於 mpegmpegts 格式的視訊流 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_VIDEOAVMEDIA_TYPE_AUDIOAVMEDIA_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

如果編譯過程中出現 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.solibijkplayer.solibijksdl.so 檔案目錄位於如下目錄:

ijkplayer-android/android/ijkplayer/ijkplayer-armv7a/src/main/libs/armeabi-v7a/libijkffmpeg.so
複製程式碼

參考

相關文章