一文讀懂 Android FFmpeg 視頻解碼過程與實(shí)戰(zhàn)分析

概述

本文首先以 FFmpeg 視頻解碼為主題训唱,主要介紹了 FFmpeg 進(jìn)行解碼視頻時(shí)的主要流程、基本原理;其次至壤,文章還講述了與 FFmpeg 視頻解碼有關(guān)的簡(jiǎn)單應(yīng)用,包括如何在原有的 FFmpeg 視頻解碼的基礎(chǔ)上按照一定時(shí)間軸順序播放視頻枢纠、如何在播放視頻時(shí)加入 seek 的邏輯像街;除此之外,文章重點(diǎn)介紹了解碼視頻時(shí)可能容易遺漏的細(xì)節(jié)晋渺,最后是簡(jiǎn)單地闡述了下如何封裝一個(gè)具有基本的視頻解碼功能的 VideoDecoder镰绎。

前言

FFmpeg

FFmpeg 是一套可以用來錄制、轉(zhuǎn)換數(shù)字音頻木西、視頻畴栖,并能將其轉(zhuǎn)化為流的開源計(jì)算機(jī)程序,它可生成用于處理和操作多媒體數(shù)據(jù)的庫(kù)八千,其中包含了先進(jìn)的音視頻解碼庫(kù)libavcodec和音視頻格式轉(zhuǎn)換庫(kù)libavformat吗讶。

FFmpeg 六大常用功能模塊

libavformat:多媒體文件或協(xié)議的封裝和解封裝庫(kù),如 mp4恋捆、flv 等文件封裝格式照皆,rtmp、rtsp 等網(wǎng)絡(luò)協(xié)議封裝格式沸停;

libavcodec:音視頻解碼核心庫(kù)膜毁;

libavfilter:音視頻、字幕濾鏡庫(kù);

libswscale:圖像格式轉(zhuǎn)換庫(kù)瘟滨;

libswresample:音頻重采樣庫(kù)葬凳;

libavutil:工具庫(kù)

視頻解碼基礎(chǔ)入門

解復(fù)用(Demux):解復(fù)用也可叫解封裝。這里有一個(gè)概念叫封裝格式室奏,封裝格式指的是音視頻的組合格式火焰,常見的有 mp4、flv胧沫、mkv 等昌简。通俗來講,封裝是將音頻流绒怨、視頻流纯赎、字幕流以及其他附件按一定規(guī)則組合成一個(gè)封裝的產(chǎn)物。而解封裝起著與封裝相反的作用南蹂,將一個(gè)流媒體文件拆解成音頻數(shù)據(jù)和視頻數(shù)據(jù)等犬金。此時(shí)拆分后數(shù)據(jù)是經(jīng)過壓縮編碼的,常見的視頻壓縮數(shù)據(jù)格式有 h264六剥。

1.png

解碼(Decode):簡(jiǎn)單來說晚顷,就是對(duì)壓縮的編碼數(shù)據(jù)解壓成原始的視頻像素?cái)?shù)據(jù),常用的原始視頻像素?cái)?shù)據(jù)格式有 yuv疗疟。
image.png

色彩空間轉(zhuǎn)換(Color Space Convert):通常對(duì)于圖像顯示器來說该默,它是通過 RGB 模型來顯示圖像的,但在傳輸圖像數(shù)據(jù)時(shí)使用 YUV 模型可以節(jié)省帶寬策彤。因此在顯示圖像時(shí)就需要將 yuv 像素格式的數(shù)據(jù)轉(zhuǎn)換成 rgb 的像素格式后再進(jìn)行渲染栓袖。
渲染(Render):將前面已經(jīng)解碼和進(jìn)行色彩空間轉(zhuǎn)換的每一個(gè)視頻幀的數(shù)據(jù)發(fā)送給顯卡以繪制在屏幕畫面上。
相關(guān)視頻推薦
<u>https</u><u>://ke.qq.com/course/3202131?flowToken=1042316</u>

一店诗、 引入 FFmpeg 前的準(zhǔn)備工作
1.1 FFmpeg so 庫(kù)編譯

在 FFmpeg 官網(wǎng)下載源碼庫(kù)并解壓裹刮;
下載 NDK 庫(kù)并解壓;
配置解壓后的 FFmpeg 源碼庫(kù)目錄中的 configure庞瘸,修改高亮部分幾個(gè)參數(shù)為以下的內(nèi)容捧弃,主要目的是生成 Android 可使用的 名稱-版本.so 文件的格式;

# ······
# build settings
SHFLAGS='-shared -Wl,-soname,$$(@F)'
LIBPREF="lib"
LIBSUF=".a"
FULLNAME='$(NAME)$(BUILDSUF)'
LIBNAME='$(LIBPREF)$(FULLNAME)$(LIBSUF)'
SLIBPREF="lib"
SLIBSUF=".so"
SLIBNAME='$(SLIBPREF)$(FULLNAME)$(SLIBSUF)'
SLIBNAME_WITH_VERSION='$(SLIBNAME).$(LIBVERSION)'

# 已修改配置
SLIBNAME_WITH_MAJOR='$(SLIBNAME)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)'
LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)'
SLIB_INSTALL_LINKS='$(SLIBNAME)'
# ······

在 FFmpeg 源碼庫(kù)目錄下新建腳本文件 build_android_arm_v8a.sh恕洲,在文件中配置 NDK 的路徑塔橡,并輸入下面其他的內(nèi)容;

# 清空上次的編譯
make clean
# 這里先配置你的 NDK 路徑
export NDK=/Users/bytedance/Library/Android/sdk/ndk/21.4.7075529
TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/darwin-x86_64


function build_android
{

./configure \
--prefix=$PREFIX \
--disable-postproc \
--disable-debug \
--disable-doc \
--enable-FFmpeg \
--disable-doc \
--disable-symver \
--disable-static \
--enable-shared \
--cross-prefix=$CROSS_PREFIX \
--target-os=android \
--arch=$ARCH \
--cpu=$CPU \
--cc=$CC \
--cxx=$CXX \
--enable-cross-compile \
--sysroot=$SYSROOT \
--extra-cflags="-Os -fpic $OPTIMIZE_CFLAGS" \
--extra-ldflags="$ADDI_LDFLAGS"

make clean
make -j16
make install

echo "============================ build android arm64-v8a success =========================="

}

# arm64-v8a
ARCH=arm64
CPU=armv8-a
API=21
CC=$TOOLCHAIN/bin/aarch64-linux-android$API-clang
CXX=$TOOLCHAIN/bin/aarch64-linux-android$API-clang++
SYSROOT=$NDK/toolchains/llvm/prebuilt/darwin-x86_64/sysroot
CROSS_PREFIX=$TOOLCHAIN/bin/aarch64-linux-android-
PREFIX=$(pwd)/android/$CPU
OPTIMIZE_CFLAGS="-march=$CPU"

echo $CC

build_android

設(shè)置 NDK 文件夾中所有文件的權(quán)限 chmod 777 -R NDK霜第;
終端執(zhí)行腳本 ./build_android_arm_v8a.sh葛家,開始編譯 FFmpeg。編譯成功后的文件會(huì)在 FFmpeg 下的 android 目錄中泌类,會(huì)出現(xiàn)多個(gè) .so 文件癞谒;


image.png

若要編譯 arm-v7a底燎,只需要拷貝修改以上的腳本為以下 build_android_arm_v7a.sh 的內(nèi)容。

#armv7-a
ARCH=arm
CPU=armv7-a
API=21
CC=$TOOLCHAIN/bin/armv7a-linux-androideabi$API-clang
CXX=$TOOLCHAIN/bin/armv7a-linux-androideabi$API-clang++
SYSROOT=$NDK/toolchains/llvm/prebuilt/darwin-x86_64/sysroot
CROSS_PREFIX=$TOOLCHAIN/bin/arm-linux-androideabi-
PREFIX=$(pwd)/android/$CPU
OPTIMIZE_CFLAGS="-mfloat-abi=softfp -mfpu=vfp -marm -march=$CPU "

分享一個(gè)音視頻高級(jí)開發(fā)交流群,需要學(xué)習(xí)資料的點(diǎn)擊788280672*加入群自取,資料包括(C/C++弹砚,Linux双仍,F(xiàn)Fmpeg webRTC rtmp hls rtsp ffplay srs 等等),免費(fèi)分享桌吃。

圖片1.png

圖片2.png

1.2 在 Android 中引入 FFmpeg 的 so 庫(kù)

NDK 環(huán)境朱沃、CMake 構(gòu)建工具、LLDB(C/C++ 代碼調(diào)試工具)茅诱;
新建 C++ module逗物,一般會(huì)生成以下幾個(gè)重要的文件:CMakeLists.txt、native-lib.cpp瑟俭、MainActivity翎卓;
在 app/src/main/ 目錄下,新建目錄摆寄,并命名 jniLibs失暴,這是 Android Studio 默認(rèn)放置 so 動(dòng)態(tài)庫(kù)的目錄;接著在 jniLibs 目錄下微饥,新建 arm64-v8a 目錄逗扒,然后將編譯好的 .so 文件粘貼至此目錄下;然后再將編譯時(shí)生成的 .h 頭文件(FFmpeg 對(duì)外暴露的接口)粘貼至 cpp 目錄下的 include 中畜号。以上的 .so 動(dòng)態(tài)庫(kù)目錄和 .h 頭文件目錄都會(huì)在 CMakeLists.txt 中顯式聲明和鏈接進(jìn)來缴阎;
最上層的 MainActivity,在這里面加載 C/C++ 代碼編譯的庫(kù):native-lib简软。native-lib 在 CMakeLists.txt 中被添加到名為 "ffmpeg" 的 library 中,所以在 System.loadLibrary()中輸入的是 "ffmpeg"述暂;

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Example of a call to a native method
        sample_text.text = stringFromJNI()
    }

    // 聲明一個(gè)外部引用的方法痹升,此方法和 C/C++ 層的代碼是對(duì)應(yīng)的。
    external fun stringFromJNI(): String

    companion object {

        // 在 init{} 中加載 C/C++ 編譯成的 library:ffmpeg
        // library 名稱的定義和添加在 CMakeLists.txt 中完成
        init {
            System.loadLibrary("ffmpeg")
        }
    }
}

native-lib.cpp 是一個(gè) C++ 接口文件畦韭,Java 層中聲明的 external 方法在這里得到實(shí)現(xiàn)疼蛾;

#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_bytedance_example_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

CMakeLists.txt 是一個(gè)構(gòu)建腳本,目的是配置可以編譯出 native-lib 此 so 庫(kù)的構(gòu)建信息艺配;

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.10.2)

# Declares and names the project.

project("ffmpeg")

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

# 定義 so 庫(kù)和頭文件所在目錄察郁,方便后面使用
set(FFmpeg_lib_dir ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
set(FFmpeg_head_dir ${CMAKE_SOURCE_DIR}/FFmpeg)

# 添加頭文件目錄
include_directories(
        FFmpeg/include
)

add_library( # Sets the name of the library.
        ffmmpeg

        # Sets the library as a shared library.
        SHARED

        # Provides a relative path to your source file(s).
        native-lib.cpp
        )

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

# 添加FFmpeg相關(guān)的so庫(kù)
add_library( avutil
        SHARED
        IMPORTED )
set_target_properties( avutil
        PROPERTIES IMPORTED_LOCATION
        ${FFmpeg_lib_dir}/libavutil.so )
add_library( swresample
        SHARED
        IMPORTED )
set_target_properties( swresample
        PROPERTIES IMPORTED_LOCATION
        ${FFmpeg_lib_dir}/libswresample.so )

add_library( avcodec
        SHARED
        IMPORTED )
set_target_properties( avcodec
        PROPERTIES IMPORTED_LOCATION
        ${FFmpeg_lib_dir}/libavcodec.so )


find_library( # Sets the name of the path variable.
        log-lib

        # Specifies the name of the NDK library that
        # you want CMake to locate.
        log)

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
        audioffmmpeg

        # 把前面添加進(jìn)來的 FFmpeg.so 庫(kù)都鏈接到目標(biāo)庫(kù) native-lib 上
        avutil
        swresample
        avcodec

        -landroid

        # Links the target library to the log library
        # included in the NDK.
        ${log-lib})

以上的操作就將 FFmpeg 引入 Android 項(xiàng)目。
二转唉、FFmpeg 解碼視頻的原理和細(xì)節(jié)
2.1 主要流程


image.png

2.2 基本原理
2.2.1 常用的 ffmpeg 接口

// 1 分配 AVFormatContext
avformat_alloc_context();
// 2 打開文件輸入流
avformat_open_input(AVFormatContext **ps, const char *url,
                        const AVInputFormat *fmt, AVDictionary **options);
// 3 提取輸入文件中的數(shù)據(jù)流信息
avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
// 4 分配編解碼上下文
avcodec_alloc_context3(const AVCodec *codec);
// 5 基于與數(shù)據(jù)流相關(guān)的編解碼參數(shù)來填充編解碼器上下文
avcodec_parameters_to_context(AVCodecContext *codec,
                                  const AVCodecParameters *par);
// 6 查找對(duì)應(yīng)已注冊(cè)的編解碼器
avcodec_find_decoder(enum AVCodecID id);
// 7 打開編解碼器
avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
// 8 不停地從碼流中提取壓縮幀數(shù)據(jù)皮钠,獲取的是一幀視頻的壓縮數(shù)據(jù)
av_read_frame(AVFormatContext *s, AVPacket *pkt);
// 9 發(fā)送原生的壓縮數(shù)據(jù)輸入到解碼器(compressed data)
avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);
// 10 接收解碼器輸出的解碼數(shù)據(jù)
avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);

2.2.2 視頻解碼的整體思路

首先要注冊(cè) libavformat 并且注冊(cè)所有的編解碼器、復(fù)用/解復(fù)用組赠法、協(xié)議等麦轰。它是所有基于 FFmpeg 的應(yīng)用程序中第一個(gè)被調(diào)用的函數(shù), 只有調(diào)用了該函數(shù),才能正常使用 FFmpeg 的各項(xiàng)功能。另外款侵,在最新版本的 FFmpeg 中目前已經(jīng)可以不用加入這行代碼末荐;

av_register_all();

打開視頻文件,提取文件中的數(shù)據(jù)流信息新锈;

auto av_format_context = avformat_alloc_context();
avformat_open_input(&av_format_context, path_.c_str(), nullptr, nullptr);
avformat_find_stream_info(av_format_context, nullptr);

然后獲取視頻媒體流的下標(biāo)甲脏,才能找到文件中的視頻媒體流;

int video_stream_index = -1;
for (int i = 0; i < av_format_context->nb_streams; i++) {
    // 匹配找到視頻媒體流的下標(biāo)妹笆,
    if (av_format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
        video_stream_index = i;
        LOGD(TAG, "find video stream index = %d", video_stream_index);
        break;
    }
}

獲取視頻媒體流剃幌、獲取解碼器上下文、獲取解碼器上下文晾浴、配置解碼器上下文的參數(shù)值负乡、打開解碼器;

// 獲取視頻媒體流
auto stream = av_format_context->streams[video_stream_index];
// 找到已注冊(cè)的解碼器
auto codec = avcodec_find_decoder(stream->codecpar->codec_id);
// 獲取解碼器上下文
AVCodecContext* codec_ctx = avcodec_alloc_context3(codec);
// 將視頻媒體流的參數(shù)配置到解碼器上下文
auto ret = avcodec_parameters_to_context(codec_ctx, stream->codecpar);

if (ret >= 0) {
    // 打開解碼器
    avcodec_open2(codec_ctx, codec, nullptr);
    // ······
}

通過指定像素格式脊凰、圖像寬抖棘、圖像高來計(jì)算所需緩沖區(qū)需要的內(nèi)存大小,分配設(shè)置緩沖區(qū)狸涌;并且由于是上屏繪制切省,因此我們需要用到 ANativeWindow,使用 ANativeWindow_setBuffersGeometry 設(shè)置此繪制窗口的屬性帕胆;

video_width_ = codec_ctx->width;
video_height_ = codec_ctx->height;

int buffer_size = av_image_get_buffer_size(AV_PIX_FMT_RGBA,
                                           video_width_, video_height_, 1);
// 輸出 buffer
out_buffer_ = (uint8_t*) av_malloc(buffer_size * sizeof(uint8_t));
// 通過設(shè)置寬高來限制緩沖區(qū)中的像素?cái)?shù)量朝捆,而非顯示屏幕的尺寸。
// 如果緩沖區(qū)與顯示的屏幕尺寸不相符懒豹,則實(shí)際顯示的可能會(huì)是拉伸芙盘,或者被壓縮的圖像
int result = ANativeWindow_setBuffersGeometry(native_window_, video_width_,
                                              video_height_, WINDOW_FORMAT_RGBA_8888);

分配內(nèi)存空間給像素格式為 RGBA 的 AVFrame,用于存放轉(zhuǎn)換成 RGBA 后的幀數(shù)據(jù)脸秽;設(shè)置 rgba_frame 緩沖區(qū)儒老,使其與 out_buffer_ 相關(guān)聯(lián);

auto rgba_frame = av_frame_alloc();
av_image_fill_arrays(rgba_frame->data, rgba_frame->linesize,
                     out_buffer_,
                     AV_PIX_FMT_RGBA,
                     video_width_, video_height_, 1);

獲取 SwsContext记餐,它在調(diào)用 sws_scale() 進(jìn)行圖像格式轉(zhuǎn)換和圖像縮放時(shí)會(huì)使用到驮樊。YUV420P 轉(zhuǎn)換為 RGBA 時(shí)可能會(huì)在調(diào)用 sws_scale 時(shí)格式轉(zhuǎn)換失敗而無法返回正確的高度值,原因跟調(diào)用 sws_getContext 時(shí) flags 有關(guān)片酝,需要將 SWS_BICUBIC 換成 SWS_FULL_CHR_H_INT | SWS_ACCURATE_RND囚衔;

struct SwsContext* data_convert_context = sws_getContext(
                    video_width_, video_height_, codec_ctx->pix_fmt,
                    video_width_, video_height_, AV_PIX_FMT_RGBA,
                    SWS_BICUBIC, nullptr, nullptr, nullptr);

分配內(nèi)存空間給用于存儲(chǔ)原始數(shù)據(jù)的 AVFrame,指向原始幀數(shù)據(jù)雕沿;并且分配內(nèi)存空間給用于存放視頻解碼前數(shù)據(jù)的 AVPacket练湿;

auto frame = av_frame_alloc();
auto packet = av_packet_alloc();

從視頻碼流中循環(huán)讀取壓縮幀數(shù)據(jù),然后開始解碼晦炊;

ret = av_read_frame(av_format_context, packet);
if (packet->size) {
    Decode(codec_ctx, packet, frame, stream, lock, data_convert_context, rgba_frame);
}

在 Decode() 函數(shù)中將裝有原生壓縮數(shù)據(jù)的 packet 作為輸入發(fā)送給解碼器鞠鲜;

/* send the packet with the compressed data to the decoder */
ret = avcodec_send_packet(codec_ctx, pkt);

解碼器返回解碼后的幀數(shù)據(jù)到指定的 frame 上宁脊,后續(xù)可對(duì)已解碼 frame 的 pts 換算為時(shí)間戳,按時(shí)間軸的顯示順序逐幀繪制到播放的畫面上贤姆;

while (ret >= 0 && !is_stop_) {
    // 返回解碼后的數(shù)據(jù)到 frame
    ret = avcodec_receive_frame(codec_ctx, frame);
    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
        return;
    } else if (ret < 0) {
        return;
    }
    // 拿到當(dāng)前解碼后的 frame榆苞,對(duì)其 pts 換算成時(shí)間戳,以便于跟傳入的指定時(shí)間戳進(jìn)行比
    auto decode_time_ms = frame->pts * 1000 / stream->time_base.den;
    if (decode_time_ms >= time_ms_) {
        last_decode_time_ms_ = decode_time_ms;
        is_seeking_ = false;
        // ······
        // 圖片數(shù)據(jù)格式轉(zhuǎn)換
        // ······
        // 把轉(zhuǎn)換后的數(shù)據(jù)繪制到屏幕上
    }
    av_packet_unref(pkt);
}

繪制畫面之前霞捡,要進(jìn)行圖片數(shù)據(jù)格式的轉(zhuǎn)換坐漏,這里就要用到前面獲取到的 SwsContext;

// 圖片數(shù)據(jù)格式轉(zhuǎn)換
int result = sws_scale(
        sws_context,
        (const uint8_t* const*) frame->data, frame->linesize,
        0, video_height_,
        rgba_frame->data, rgba_frame->linesize);

if (result <= 0) {
    LOGE(TAG, "Player Error : data convert fail");
    return;
}

因?yàn)槭巧掀晾L制碧信,所以用到了 ANativeWindow 和 ANativeWindow_Buffer赊琳。在繪制畫面之前,需要使用鎖定窗口的下一個(gè)繪圖 surface 以進(jìn)行繪制砰碴,然后將要顯示的幀數(shù)據(jù)寫入到緩沖區(qū)中躏筏,最后解鎖窗口的繪圖 surface,將緩沖區(qū)的數(shù)據(jù)發(fā)布到屏幕顯示上呈枉;

// 播放
result = ANativeWindow_lock(native_window_, &window_buffer_, nullptr);
if (result < 0) {
    LOGE(TAG, "Player Error : Can not lock native window");
} else {
    // 將圖像繪制到界面上
    // 注意 : 這里 rgba_frame 一行的像素和 window_buffer 一行的像素長(zhǎng)度可能不一致
    // 需要轉(zhuǎn)換好 否則可能花屏
    auto bits = (uint8_t*) window_buffer_.bits;
    for (int h = 0; h < video_height_; h++) {
        memcpy(bits + h * window_buffer_.stride * 4,
               out_buffer_ + h * rgba_frame->linesize[0],
               rgba_frame->linesize[0]);
    }
    ANativeWindow_unlockAndPost(native_window_);
}

以上就是主要的解碼過程趁尼。除此之外,因?yàn)?C++ 使用資源和內(nèi)存空間時(shí)需要自行釋放猖辫,所以解碼結(jié)束后還需要調(diào)用釋放的接口釋放資源酥泞,以免造成內(nèi)存泄漏。

sws_freeContext(data_convert_context);
av_free(out_buffer_);
av_frame_free(&rgba_frame);
av_frame_free(&frame);
av_packet_free(&packet);

avcodec_close(codec_ctx);
avcodec_free_context(&codec_ctx);

avformat_close_input(&av_format_context);
avformat_free_context(av_format_context);
ANativeWindow_release(native_window_);

2.3 簡(jiǎn)單應(yīng)用
為了更好地理解視頻解碼的過程啃憎,這里封裝一個(gè)視頻解碼器 VideoDecoder 芝囤,解碼器初步會(huì)有以下幾個(gè)函數(shù):

VideoDecoder(const char* path, std::function<void(long timestamp)> on_decode_frame);

void Prepare(ANativeWindow* window);

bool DecodeFrame(long time_ms);

void Release();

在這個(gè)視頻解碼器中,輸入指定時(shí)間戳后會(huì)返回解碼的這一幀數(shù)據(jù)辛萍。其中較為重要的是 DecodeFrame(long time_ms) 函數(shù)悯姊,它可以由使用者自行調(diào)用,傳入指定幀的時(shí)間戳叹阔,進(jìn)而解碼對(duì)應(yīng)的幀數(shù)據(jù)挠轴。此外,可以增加同步鎖以實(shí)現(xiàn)解碼線程和使用線程分離耳幢。
2.3.1 加入同步鎖實(shí)現(xiàn)視頻播放
若只要對(duì)視頻進(jìn)行解碼,是不需要使用同步等待的欧啤;
但若是要實(shí)現(xiàn)視頻的播放睛藻,那么每解碼繪制完一幀就需使用鎖進(jìn)行同步等待,這是因?yàn)椴シ乓曨l時(shí)需要讓解碼和繪制分離邢隧、且按照一定的時(shí)間軸順序和速度進(jìn)行解碼和繪制店印。

condition_.wait(lock);

在上層調(diào)用 DecodeFrame 函數(shù)傳入解碼的時(shí)間戳?xí)r喚醒同步鎖,讓解碼繪制的循環(huán)繼續(xù)執(zhí)行倒慧。

bool VideoDecoder::DecodeFrame(long time_ms) {
    // ······
    time_ms_ = time_ms;
    condition_.notify_all();
    return true;
}

2.3.2 播放時(shí)加入 seek_frame
在正常播放情況下按摘,視頻是一幀一幀逐幀解碼播放包券;但在拖動(dòng)進(jìn)度條到達(dá)指定的 seek 點(diǎn)的情況下,如果還是從頭到尾逐幀解碼到 seek 點(diǎn)的話炫贤,效率可能不太高溅固。這時(shí)候就需要在一定規(guī)則內(nèi)對(duì) seek 點(diǎn)的時(shí)間戳做檢查,符合條件的直接 seek 到指定的時(shí)間戳兰珍。
FFmpeg 中的 av_seek_frame

av_seek_frame 可以定位到關(guān)鍵幀和非關(guān)鍵幀侍郭,這取決于選擇的 flag 值。因?yàn)橐曨l的解碼需要依賴關(guān)鍵幀掠河,所以一般我們需要定位到關(guān)鍵幀亮元;

int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp,
                  int flags);

av_seek_frame 中的 flag 是用來指定尋找的 I 幀和傳入的時(shí)間戳之間的位置關(guān)系。當(dāng)要 seek 已過去的時(shí)間戳?xí)r唠摹,時(shí)間戳不一定會(huì)剛好處在 I 幀的位置爆捞,但因?yàn)榻獯a需要依賴 I 幀,所以需要先找到此時(shí)間戳附近一個(gè)的 I 幀勾拉,此時(shí) flag 就表明要 seek 到當(dāng)前時(shí)間戳的前一個(gè) I 幀還是后一個(gè) I 幀煮甥;
flag 有四個(gè)選項(xiàng):


image.png

flag 可能同時(shí)包含以上的多個(gè)值。比如 AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_BYTE望艺;
FRAME 和 BACKWARD 是按幀之間的間隔推算出 seek 的目標(biāo)位置苛秕,適合快進(jìn)快退;BYTE 則適合大幅度滑動(dòng)找默。

seek 的場(chǎng)景

解碼時(shí)傳入的時(shí)間戳若是往前進(jìn)的方向艇劫,并且超過上一幀時(shí)間戳有一定距離就需要 seek,這里的“一定距離”是通過多次實(shí)驗(yàn)估算所得惩激,并非都是以下代碼中使用的 1000ms店煞;
如果是往后退的方向且小于上一次解碼時(shí)間戳,但與上一次解碼時(shí)間戳的距離比較大(比如已超過 50ms)风钻,就要 seek 到上一個(gè)關(guān)鍵幀顷蟀;
使用 bool 變量 is_seeking_ 是為了防止其他干擾當(dāng)前 seeking 的操作,目的是控制當(dāng)前只有一個(gè) seek 操作在進(jìn)行骡技。

if (!is_seeking_ && (time_ms_ > last_decode_time_ms_ + 1000 ||
                     time_ms_ < last_decode_time_ms_ - 50)) {
    is_seeking_ = true;
    // seek 時(shí)傳入的是指定幀帶有 time_base 的時(shí)間戳鸣个,因此要用 times_ms 進(jìn)行推算
    LOGD(TAG, "seek frame time_ms_ = %ld, last_decode_time_ms_ = %ld", time_ms_,
         last_decode_time_ms_);
    av_seek_frame(av_format_context,
                  video_stream_index,
                  time_ms_ * stream->time_base.den / 1000,
                  AVSEEK_FLAG_BACKWARD);
}

插入 seek 的邏輯
因?yàn)樵诮獯a前要檢查是否 seek布朦,所以要在 av_read_frame 函數(shù)(返回視頻媒體流下一幀)之前插入 seek 的邏輯囤萤,符合 seek 條件時(shí)使用 av_seek_frame 到達(dá)指定 I 幀,接著 av_read_frame 后再繼續(xù)解碼到目的時(shí)間戳的位置是趴。

// 是否進(jìn)行 seek 的邏輯寫在這
// 接下來是讀取視頻流的下一幀
int ret = av_read_frame(av_format_context, packet);

2.4 解碼過程中的細(xì)節(jié)
2.4.1 DecodeFrame 時(shí) seek 的條件
使用 av_seek_frame 函數(shù)時(shí)需要指定正確的 flag涛舍,并且還要約定進(jìn)行 seek 操作時(shí)的條件,否則視頻可能會(huì)出現(xiàn)花屏(馬賽克)唆途。

if (!is_seeking_ && (time_ms_ > last_decode_time_ms_ + 1000 ||
                     time_ms_ < last_decode_time_ms_ - 50)) {
    is_seeking_ = true;
    av_seek_frame(···,···,···,AVSEEK_FLAG_BACKWARD);
}

2.4.2 減少解碼的次數(shù)
在視頻解碼時(shí)富雅,在有些條件下是可以不用對(duì)傳入時(shí)間戳的幀數(shù)據(jù)進(jìn)行解碼的掸驱。比如:

當(dāng)前解碼時(shí)間戳若是前進(jìn)方向并且與上一次的解碼時(shí)間戳相同或者與當(dāng)前正在解碼的時(shí)間戳相同,則不需要進(jìn)行解碼没佑;
當(dāng)前解碼時(shí)間戳若不大于上一次的解碼時(shí)間戳并且與上一次的解碼時(shí)間戳之間的距離相差較斜显簟(比如未超過 50ms),則不需要進(jìn)行解碼图筹。

bool VideoDecoder::DecodeFrame(long time_ms) {
    LOGD(TAG, "DecodeFrame time_ms = %ld", time_ms);
    if (last_decode_time_ms_ == time_ms || time_ms_ == time_ms) {
        LOGD(TAG, "DecodeFrame last_decode_time_ms_ == time_ms");
        return false;
    }
    if (time_ms <= last_decode_time_ms_ &&
        time_ms + 50 >= last_decode_time_ms_) {
        return false;
    }
    time_ms_ = time_ms;
    condition_.notify_all();
    return true;
}

有了以上這些條件的約束后帅刀,會(huì)減少一些不必要的解碼操作。
2.4.3 使用 AVFrame 的 pts

AVPacket 存儲(chǔ)解碼前的數(shù)據(jù)(編碼數(shù)據(jù):H264/AAC 等)远剩,保存的是解封裝之后扣溺、解碼前的數(shù)據(jù),仍然是壓縮數(shù)據(jù)瓜晤;AVFrame 存儲(chǔ)解碼后的數(shù)據(jù)(像素?cái)?shù)據(jù):YUV/RGB/PCM 等)锥余;
AVPacket 的 pts 和 AVFrame 的 pts 意義存在差異。前者表示這個(gè)解壓包何時(shí)顯示痢掠,后者表示幀數(shù)據(jù)何時(shí)顯示驱犹;

// AVPacket 的 pts
   /**
    * Presentation timestamp in AVStream->time_base units; the time at which
    * the decompressed packet will be presented to the user.
    * Can be AV_NOPTS_VALUE if it is not stored in the file.
    * pts MUST be larger or equal to dts as presentation cannot happen before
    * decompression, unless one wants to view hex dumps. Some formats misuse
    * the terms dts and pts/cts to mean something different. Such timestamps
    * must be converted to true pts/dts before they are stored in AVPacket.
    */
   int64_t pts;

   // AVFrame 的 pts
   /**
    * Presentation timestamp in time_base units (time when frame should be shown to user).
    */
   int64_t pts;

是否將當(dāng)前解碼的幀數(shù)據(jù)繪制到畫面上,取決于傳入到解碼時(shí)間戳與當(dāng)前解碼器返回的已解碼幀的時(shí)間戳的比較結(jié)果足画。這里不可使用 AVPacket 的 pts雄驹,它很可能不是一個(gè)遞增的時(shí)間戳;
需要進(jìn)行畫面繪制的前提是:當(dāng)傳入指定的解碼時(shí)間戳不大于當(dāng)前已解碼 frame 的 pts 換算后的時(shí)間戳?xí)r進(jìn)行畫面繪制淹辞。

auto decode_time_ms = frame->pts * 1000 / stream->time_base.den;
LOGD(TAG, "decode_time_ms = %ld", decode_time_ms);
if (decode_time_ms >= time_ms_) {
    last_decode_time_ms_ = decode_time_ms;
    is_seeking = false;
    // 畫面繪制
    // ····
}

2.4.4 解碼最后一幀時(shí)視頻已經(jīng)沒有數(shù)據(jù)
使用 av_read_frame(av_format_context, packet)返回視頻媒體流下一幀到 AVPacket 中医舆。如果函數(shù)返回的 int 值是 0 則是 Success,如果小于 0 則是 Error 或者 EOF象缀。
因此如果在播放視頻時(shí)返回的是小于 0 的值蔬将,調(diào)用 avcodec_flush_buffers 函數(shù)重置解碼器的狀態(tài),flush 緩沖區(qū)中的內(nèi)容央星,然后再 seek 到當(dāng)前傳入的時(shí)間戳處霞怀,完成解碼后的回調(diào),再讓同步鎖進(jìn)行等待莉给。

// 讀取碼流中的音頻若干幀或者視頻一幀毙石,
// 這里是讀取視頻一幀(完整的一幀),獲取的是一幀視頻的壓縮數(shù)據(jù)颓遏,接下來才能對(duì)其進(jìn)行解碼
ret = av_read_frame(av_format_context, packet);
if (ret < 0) {
    avcodec_flush_buffers(codec_ctx);
    av_seek_frame(av_format_context, video_stream_index,
                  time_ms_ * stream->time_base.den / 1000, AVSEEK_FLAG_BACKWARD);
    LOGD(TAG, "ret < 0, condition_.wait(lock)");
    // 防止解最后一幀時(shí)視頻已經(jīng)沒有數(shù)據(jù)
    on_decode_frame_(last_decode_time_ms_);
    condition_.wait(lock);
}

2.5 上層封裝解碼器 VideoDecoder
如果要在上層封裝一個(gè) VideoDecoder胁黑,只需要將 C++ 層 VideoDecoder 的接口暴露在 native-lib.cpp 中,然后上層通過 JNI 的方式調(diào)用 C++ 的接口州泊。
比如上層要傳入指定的解碼時(shí)間戳進(jìn)行解碼時(shí),寫一個(gè) deocodeFrame 方法漂洋,然后把時(shí)間戳傳到 C++ 層的 nativeDecodeFrame 進(jìn)行解碼遥皂,而 nativeDecodeFrame 這個(gè)方法的實(shí)現(xiàn)就寫在 native-lib.cpp 中力喷。

// FFmpegVideoDecoder.kt
class FFmpegVideoDecoder(
    path: String,
    val onDecodeFrame: (timestamp: Long, texture: SurfaceTexture, needRender: Boolean) -> Unit
){
    // 抽第 timeMs 幀,根據(jù) sync 是否同步等待
    fun decodeFrame(timeMS: Long, sync: Boolean = false) {
        // 若當(dāng)前不需要抽幀時(shí)不進(jìn)行等待
        if (nativeDecodeFrame(decoderPtr, timeMS) && sync) {
            // ······
    } else {
            // ······
        }
    }

    private external fun nativeDecodeFrame(decoder: Long, timeMS: Long): Boolean

    companion object {
        const val TAG = "FFmpegVideoDecoder"

        init {
            System.loadLibrary("ffmmpeg")

        }
    }
}

然后在 native-lib.cpp 中調(diào)用 C++ 層 VideoDecoder 的接口 DecodeFrame 演训,這樣就通過 JNI 的方式建立起了上層和 C++ 底層之間的聯(lián)系

// native-lib.cpp
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_example_decoder_video_FFmpegVideoDecoder_nativeDecodeFrame(JNIEnv* env,
                                                               jobject thiz,
                                                               jlong decoder,
                                                               jlong time_ms) {
    auto videoDecoder = (codec::VideoDecoder*)decoder;
    return videoDecoder->DecodeFrame(time_ms);
}

三弟孟、心得

技術(shù)經(jīng)驗(yàn)

FFmpeg 編譯后與 Android 結(jié)合起來實(shí)現(xiàn)視頻的解碼播放,便捷性很高样悟。
由于是用 C++ 層實(shí)現(xiàn)具體的解碼流程拂募,會(huì)有學(xué)習(xí)難度,最好有一定的 C++ 基礎(chǔ)窟她。

四陈症、附錄

C++ 封裝的 VideoDecoder

VideoDecoder.h

#include <jni.h>
#include <mutex>
#include <android/native_window.h>
#include <android/native_window_jni.h>
#include <time.h>

extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswresample/swresample.h>
#include <libswscale/swscale.h>
}
#include <string>
/*
 * VideoDecoder 可用于解碼某個(gè)音視頻文件(比如.mp4)中視頻媒體流的數(shù)據(jù)。
 * Java 層傳入指定文件的路徑后震糖,可以按一定 fps 循環(huán)傳入指定的時(shí)間戳進(jìn)行解碼(抽幀)录肯,這一實(shí)現(xiàn)由 C++ 提供的 DecodeFrame 來完成。
 * 在每次解碼結(jié)束時(shí)吊说,將解碼某一幀的時(shí)間戳回調(diào)給上層的解碼器论咏,以供其他操作使用。
 */
namespace codec {
class VideoDecoder {

private:
    std::string path_;
    long time_ms_ = -1;
    long last_decode_time_ms_ = -1;
    bool is_seeking_ = false;
    ANativeWindow* native_window_ = nullptr;
    ANativeWindow_Buffer window_buffer_{};颁井、
    // 視頻寬高屬性
    int video_width_ = 0;
    int video_height_ = 0;
    uint8_t* out_buffer_ = nullptr;
    // on_decode_frame 用于將抽取指定幀的時(shí)間戳回調(diào)給上層解碼器厅贪,以供上層解碼器進(jìn)行其他操作。
    std::function<void(long timestamp)> on_decode_frame_ = nullptr;
    bool is_stop_ = false;

    // 會(huì)與在循環(huán)同步時(shí)用的鎖 “std::unique_lock<std::mutex>” 配合使用
    std::mutex work_queue_mtx;
    // 真正在進(jìn)行同步等待和喚醒的屬性
    std::condition_variable condition_;
    // 解碼器真正進(jìn)行解碼的函數(shù)
    void Decode(AVCodecContext* codec_ctx, AVPacket* pkt, AVFrame* frame, AVStream* stream,
                std::unique_lock<std::mutex>& lock, SwsContext* sws_context, AVFrame* pFrame);

public:
    // 新建解碼器時(shí)要傳入媒體文件路徑和一個(gè)解碼后的回調(diào) on_decode_frame雅宾。
    VideoDecoder(const char* path, std::function<void(long timestamp)> on_decode_frame);
    // 在 JNI 層將上層傳入的 Surface 包裝后新建一個(gè) ANativeWindow 傳入养涮,在后面解碼后繪制幀數(shù)據(jù)時(shí)需要用到
    void Prepare(ANativeWindow* window);
    // 抽取指定時(shí)間戳的視頻幀,可由上層調(diào)用
    bool DecodeFrame(long time_ms);
    // 釋放解碼器資源
    void Release();
    // 獲取當(dāng)前系統(tǒng)毫秒時(shí)間
    static int64_t GetCurrentMilliTime(void);
};

}

VideoDecoder.cpp

#include "VideoDecoder.h"
#include "../log/Logger.h"
#include <thread>
#include <utility>

extern "C" {
#include <libavutil/imgutils.h>
}

#define TAG "VideoDecoder"
namespace codec {

VideoDecoder::VideoDecoder(const char* path, std::function<void(long timestamp)> on_decode_frame)
        : on_decode_frame_(std::move(on_decode_frame)) {
    path_ = std::string(path);
}

void VideoDecoder::Decode(AVCodecContext* codec_ctx, AVPacket* pkt, AVFrame* frame, AVStream* stream,
                     std::unique_lock<std::mutex>& lock, SwsContext* sws_context,
                     AVFrame* rgba_frame) {

    int ret;
    /* send the packet with the compressed data to the decoder */
    ret = avcodec_send_packet(codec_ctx, pkt);
    if (ret == AVERROR(EAGAIN)) {
        LOGE(TAG,
             "Decode: Receive_frame and send_packet both returned EAGAIN, which is an API violation.");
    } else if (ret < 0) {
        return;
    }

    // read all the output frames (infile general there may be any number of them
    while (ret >= 0 && !is_stop_) {
        // 對(duì)于frame, avcodec_receive_frame內(nèi)部每次都先調(diào)用
        ret = avcodec_receive_frame(codec_ctx, frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return;
        } else if (ret < 0) {
            return;
        }
        int64_t startTime = GetCurrentMilliTime();
        LOGD(TAG, "decodeStartTime: %ld", startTime);
        // 換算當(dāng)前解碼的frame時(shí)間戳
        auto decode_time_ms = frame->pts * 1000 / stream->time_base.den;
        LOGD(TAG, "decode_time_ms = %ld", decode_time_ms);
        if (decode_time_ms >= time_ms_) {
            LOGD(TAG, "decode decode_time_ms = %ld, time_ms_ = %ld", decode_time_ms, time_ms_);
            last_decode_time_ms_ = decode_time_ms;
            is_seeking_ = false;

            // 數(shù)據(jù)格式轉(zhuǎn)換
            int result = sws_scale(
                    sws_context,
                    (const uint8_t* const*) frame->data, frame->linesize,
                    0, video_height_,
                    rgba_frame->data, rgba_frame->linesize);

            if (result <= 0) {
                LOGE(TAG, "Player Error : data convert fail");
                return;
            }

            // 播放
            result = ANativeWindow_lock(native_window_, &window_buffer_, nullptr);
            if (result < 0) {
                LOGE(TAG, "Player Error : Can not lock native window");
            } else {
                // 將圖像繪制到界面上
                auto bits = (uint8_t*) window_buffer_.bits;
                for (int h = 0; h < video_height_; h++) {
                    memcpy(bits + h * window_buffer_.stride * 4,
                           out_buffer_ + h * rgba_frame->linesize[0],
                           rgba_frame->linesize[0]);
                }
                ANativeWindow_unlockAndPost(native_window_);
            }
            on_decode_frame_(decode_time_ms);
            int64_t endTime = GetCurrentMilliTime();
            LOGD(TAG, "decodeEndTime - decodeStartTime: %ld", endTime - startTime);
            LOGD(TAG, "finish decode frame");
            condition_.wait(lock);
        }
        // 主要作用是清理AVPacket中的所有空間數(shù)據(jù)秀又,清理完畢后進(jìn)行初始化操作单寂,并且將 data 與 size 置為0,方便下次調(diào)用吐辙。
        // 釋放 packet 引用
        av_packet_unref(pkt);
    }
}

void VideoDecoder::Prepare(ANativeWindow* window) {
    native_window_ = window;
    av_register_all();
    auto av_format_context = avformat_alloc_context();
    avformat_open_input(&av_format_context, path_.c_str(), nullptr, nullptr);
    avformat_find_stream_info(av_format_context, nullptr);
    int video_stream_index = -1;
    for (int i = 0; i < av_format_context->nb_streams; i++) {
        // 找到視頻媒體流的下標(biāo)
        if (av_format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            video_stream_index = i;
            LOGD(TAG, "find video stream index = %d", video_stream_index);
            break;
        }
    }

    // run once
    do {
        if (video_stream_index == -1) {
            codec::LOGE(TAG, "Player Error : Can not find video stream");
            break;
        }
        std::unique_lock<std::mutex> lock(work_queue_mtx);

        // 獲取視頻媒體流
        auto stream = av_format_context->streams[video_stream_index];
        // 找到已注冊(cè)的解碼器
        auto codec = avcodec_find_decoder(stream->codecpar->codec_id);
        // 獲取解碼器上下文
        AVCodecContext* codec_ctx = avcodec_alloc_context3(codec);
        auto ret = avcodec_parameters_to_context(codec_ctx, stream->codecpar);

        if (ret >= 0) {
            // 打開
            avcodec_open2(codec_ctx, codec, nullptr);
            // 解碼器打開后才有寬高的值
            video_width_ = codec_ctx->width;
            video_height_ = codec_ctx->height;

            AVFrame* rgba_frame = av_frame_alloc();
            int buffer_size = av_image_get_buffer_size(AV_PIX_FMT_RGBA, video_width_, video_height_,
                                                       1);
            // 分配內(nèi)存空間給輸出 buffer
            out_buffer_ = (uint8_t*) av_malloc(buffer_size * sizeof(uint8_t));
            av_image_fill_arrays(rgba_frame->data, rgba_frame->linesize, out_buffer_,
                                 AV_PIX_FMT_RGBA,
                                 video_width_, video_height_, 1);

            // 通過設(shè)置寬高限制緩沖區(qū)中的像素?cái)?shù)量宣决,而非屏幕的物理顯示尺寸。
            // 如果緩沖區(qū)與物理屏幕的顯示尺寸不相符昏苏,則實(shí)際顯示可能會(huì)是拉伸尊沸,或者被壓縮的圖像
            int result = ANativeWindow_setBuffersGeometry(native_window_, video_width_,
                                                          video_height_, WINDOW_FORMAT_RGBA_8888);
            if (result < 0) {
                LOGE(TAG, "Player Error : Can not set native window buffer");
                avcodec_close(codec_ctx);
                avcodec_free_context(&codec_ctx);
                av_free(out_buffer_);
                break;
            }

            auto frame = av_frame_alloc();
            auto packet = av_packet_alloc();

            struct SwsContext* data_convert_context = sws_getContext(
                    video_width_, video_height_, codec_ctx->pix_fmt,
                    video_width_, video_height_, AV_PIX_FMT_RGBA,
                    SWS_BICUBIC, nullptr, nullptr, nullptr);
            while (!is_stop_) {
                LOGD(TAG, "front seek time_ms_ = %ld, last_decode_time_ms_ = %ld", time_ms_,
                     last_decode_time_ms_);
                if (!is_seeking_ && (time_ms_ > last_decode_time_ms_ + 1000 ||
                                     time_ms_ < last_decode_time_ms_ - 50)) {
                    is_seeking_ = true;
                    LOGD(TAG, "seek frame time_ms_ = %ld, last_decode_time_ms_ = %ld", time_ms_,
                         last_decode_time_ms_);
                    // 傳進(jìn)去的是指定幀帶有 time_base 的時(shí)間戳贤惯,所以是要將原來的 times_ms 按照上面獲取時(shí)的計(jì)算方式反推算出時(shí)間戳
                    av_seek_frame(av_format_context, video_stream_index,
                                  time_ms_ * stream->time_base.den / 1000, AVSEEK_FLAG_BACKWARD);
                }
                // 讀取視頻一幀(完整的一幀)洼专,獲取的是一幀視頻的壓縮數(shù)據(jù),接下來才能對(duì)其進(jìn)行解碼
                ret = av_read_frame(av_format_context, packet);
                if (ret < 0) {
                    avcodec_flush_buffers(codec_ctx);
                    av_seek_frame(av_format_context, video_stream_index,
                                  time_ms_ * stream->time_base.den / 1000, AVSEEK_FLAG_BACKWARD);
                    LOGD(TAG, "ret < 0, condition_.wait(lock)");
                    // 防止解碼最后一幀時(shí)視頻已經(jīng)沒有數(shù)據(jù)
                    on_decode_frame_(last_decode_time_ms_);
                    condition_.wait(lock);
                }
                if (packet->size) {
                    Decode(codec_ctx, packet, frame, stream, lock, data_convert_context,
                           rgba_frame);
                }
            }
            // 釋放資源
            sws_freeContext(data_convert_context);
            av_free(out_buffer_);
            av_frame_free(&rgba_frame);
            av_frame_free(&frame);
            av_packet_free(&packet);

        }
        avcodec_close(codec_ctx);
        avcodec_free_context(&codec_ctx);

    } while (false);
    avformat_close_input(&av_format_context);
    avformat_free_context(av_format_context);
    ANativeWindow_release(native_window_);
    delete this;
}

bool VideoDecoder::DecodeFrame(long time_ms) {
    LOGD(TAG, "DecodeFrame time_ms = %ld", time_ms);
    if (last_decode_time_ms_ == time_ms || time_ms_ == time_ms) {
        LOGD(TAG, "DecodeFrame last_decode_time_ms_ == time_ms");
        return false;
    }
    if (last_decode_time_ms_ >= time_ms && last_decode_time_ms_ <= time_ms + 50) {
        return false;
    }
    time_ms_ = time_ms;
    condition_.notify_all();
    return true;
}

void VideoDecoder::Release() {
    is_stop_ = true;
    condition_.notify_all();
}

/**
 * 獲取當(dāng)前的毫秒級(jí)時(shí)間
 */
int64_t VideoDecoder::GetCurrentMilliTime(void) {
    struct timeval tv{};
    gettimeofday(&tv, nullptr);
    return tv.tv_sec * 1000.0 + tv.tv_usec / 1000.0;
}

}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末孵构,一起剝皮案震驚了整個(gè)濱河市屁商,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌颈墅,老刑警劉巖蜡镶,帶你破解...
    沈念sama閱讀 219,270評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件雾袱,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡官还,警方通過查閱死者的電腦和手機(jī)芹橡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來望伦,“玉大人林说,你說我怎么就攤上這事⊥蜕。” “怎么了腿箩?”我有些...
    開封第一講書人閱讀 165,630評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)愕掏。 經(jīng)常有香客問我度秘,道長(zhǎng),這世上最難降的妖魔是什么饵撑? 我笑而不...
    開封第一講書人閱讀 58,906評(píng)論 1 295
  • 正文 為了忘掉前任剑梳,我火速辦了婚禮,結(jié)果婚禮上滑潘,老公的妹妹穿的比我還像新娘垢乙。我一直安慰自己,他們只是感情好语卤,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評(píng)論 6 392
  • 文/花漫 我一把揭開白布追逮。 她就那樣靜靜地躺著,像睡著了一般粹舵。 火紅的嫁衣襯著肌膚如雪钮孵。 梳的紋絲不亂的頭發(fā)上琅锻,一...
    開封第一講書人閱讀 51,718評(píng)論 1 305
  • 那天慢叨,我揣著相機(jī)與錄音,去河邊找鬼扭倾。 笑死诅需,一個(gè)胖子當(dāng)著我的面吹牛漾唉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播堰塌,決...
    沈念sama閱讀 40,442評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼赵刑,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了场刑?” 一聲冷哼從身側(cè)響起般此,我...
    開封第一講書人閱讀 39,345評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后恤煞,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體屎勘,經(jīng)...
    沈念sama閱讀 45,802評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評(píng)論 3 337
  • 正文 我和宋清朗相戀三年居扒,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片丑慎。...
    茶點(diǎn)故事閱讀 40,117評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡喜喂,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出竿裂,到底是詐尸還是另有隱情玉吁,我是刑警寧澤,帶...
    沈念sama閱讀 35,810評(píng)論 5 346
  • 正文 年R本政府宣布腻异,位于F島的核電站进副,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏悔常。R本人自食惡果不足惜影斑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望机打。 院中可真熱鬧矫户,春花似錦、人聲如沸残邀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)芥挣。三九已至驱闷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間空免,已是汗流浹背空另。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鼓蜒,地道東北人痹换。 一個(gè)月前我還...
    沈念sama閱讀 48,377評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像都弹,于是被迫代替她去往敵國(guó)和親娇豫。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容