一文读懂 Android FFmpeg 视频解码过程与实战分析

PREFIX= ( p w d ) / a n d r o i d / (pwd)/android/ (pwd)/android/CPU

OPTIMIZE_CFLAGS=“-march=$CPU”

echo $CC

build_android

  • 设置 NDK 文件夹中所有文件的权限 chmod 777 -R NDK

  • 终端执行脚本 ./build_android_arm_v8a.sh,开始编译 FFmpeg。编译成功后的文件会在 FFmpeg 下的 android 目录中,会出现多个 .so 文件;

1030425c40671c4fad5aed31c9c0dd0b.png

  • 若要编译 arm-v7a,只需要拷贝修改以上的脚本为以下 build_android_arm_v7a.sh 的内容。

#armv7-a

ARCH=arm

CPU=armv7-a

API=21

CC= T O O L C H A I N / b i n / a r m v 7 a − l i n u x − a n d r o i d e a b i TOOLCHAIN/bin/armv7a-linux-androideabi TOOLCHAIN/bin/armv7alinuxandroideabiAPI-clang

CXX= T O O L C H A I N / b i n / a r m v 7 a − l i n u x − a n d r o i d e a b i TOOLCHAIN/bin/armv7a-linux-androideabi TOOLCHAIN/bin/armv7alinuxandroideabiAPI-clang++

SYSROOT=$NDK/toolchains/llvm/prebuilt/darwin-x86_64/sysroot

CROSS_PREFIX=$TOOLCHAIN/bin/arm-linux-androideabi-

PREFIX= ( p w d ) / a n d r o i d / (pwd)/android/ (pwd)/android/CPU

OPTIMIZE_CFLAGS="-mfloat-abi=softfp -mfpu=vfp -marm -march=$CPU "

1.2 在 Android 中引入 FFmpeg 的 so 库

  • NDK 环境、CMake 构建工具、LLDB(C/C++ 代码调试工具);

  • 新建 C++ module,一般会生成以下几个重要的文件:CMakeLists.txtnative-lib.cppMainActivity

  • app/src/main/ 目录下,新建目录,并命名 jniLibs,这是 Android Studio 默认放置 so 动态库的目录;接着在 jniLibs 目录下,新建 arm64-v8a 目录,然后将编译好的 .so 文件粘贴至此目录下;然后再将编译时生成的 .h 头文件(FFmpeg 对外暴露的接口)粘贴至 cpp 目录下的 include 中。以上的 .so 动态库目录和 .h 头文件目录都会在 CMakeLists.txt 中显式声明和链接进来;

  • 最上层的 MainActivity,在这里面加载 C/C++ 代码编译的库:native-libnative-libCMakeLists.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()

}

// 声明一个外部引用的方法,此方法和 C/C++ 层的代码是对应的。

external fun stringFromJNI(): String

companion object {

// 在 init{} 中加载 C/C++ 编译成的 library:ffmpeg

// library 名称的定义和添加在 CMakeLists.txt 中完成

init {

System.loadLibrary(“ffmpeg”)

}

}

}

  • native-lib.cpp 是一个 C++ 接口文件,Java 层中声明的 external 方法在这里得到实现;

#include <jni.h>

#include 

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 是一个构建脚本,目的是配置可以编译出 native-lib 此 so 库的构建信息;

# 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 库和头文件所在目录,方便后面使用

set(FFmpeg_lib_dir  C M A K E S O U R C E D I R / . . / j n i L i b s / {CMAKE_SOURCE_DIR}/../jniLibs/ CMAKESOURCEDIR/../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相关的so库

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

# 把前面添加进来的 FFmpeg.so 库都链接到目标库 native-lib 上

avutil

swresample

avcodec

-landroid

# Links the target library to the log library

# included in the NDK.

${log-lib})

  • 以上的操作就将 FFmpeg 引入 Android 项目。

二、FFmpeg 解码视频的原理和细节


2.1 主要流程

9f7adccbee12a8d8d1370ebf7c3a0e51.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 提取输入文件中的数据流信息

avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);

// 4 分配编解码上下文

avcodec_alloc_context3(const AVCodec *codec);

// 5 基于与数据流相关的编解码参数来填充编解码器上下文

avcodec_parameters_to_context(AVCodecContext *codec,

const AVCodecParameters *par);

// 6 查找对应已注册的编解码器

avcodec_find_decoder(enum AVCodecID id);

// 7 打开编解码器

avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);

// 8 不停地从码流中提取压缩帧数据,获取的是一帧视频的压缩数据

av_read_frame(AVFormatContext *s, AVPacket *pkt);

// 9 发送原生的压缩数据输入到解码器(compressed data)

avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);

// 10 接收解码器输出的解码数据

avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);

2.2.2 视频解码的整体思路
  • 首先要注册 libavformat 并且注册所有的编解码器、复用/解复用组、协议等。它是所有基于 FFmpeg 的应用程序中第一个被调用的函数, 只有调用了该函数,才能正常使用 FFmpeg 的各项功能。另外,在最新版本的 FFmpeg 中目前已经可以不用加入这行代码;

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++) {

// 匹配找到视频媒体流的下标,

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;

}

}

  • 获取视频媒体流、获取解码器上下文、获取解码器上下文、配置解码器上下文的参数值、打开解码器;

// 获取视频媒体流

auto stream = av_format_context->streams[video_stream_index];

// 找到已注册的解码器

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);

// ······

}

  • 通过指定像素格式、图像宽、图像高来计算所需缓冲区需要的内存大小,分配设置缓冲区;并且由于是上屏绘制,因此我们需要用到 ANativeWindow,使用 ANativeWindow_setBuffersGeometry 设置此绘制窗口的属性;

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));

// 通过设置宽高来限制缓冲区中的像素数量,而非显示屏幕的尺寸。

// 如果缓冲区与显示的屏幕尺寸不相符,则实际显示的可能会是拉伸,或者被压缩的图像

int result = ANativeWindow_setBuffersGeometry(native_window_, video_width_,

video_height_, WINDOW_FORMAT_RGBA_8888);

  • 分配内存空间给像素格式为 RGBA 的 AVFrame,用于存放转换成 RGBA 后的帧数据;设置 rgba_frame 缓冲区,使其与 out_buffer_ 相关联;

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,它在调用 sws_scale() 进行图像格式转换和图像缩放时会使用到。YUV420P 转换为 RGBA 时可能会在调用 sws_scale 时格式转换失败而无法返回正确的高度值,原因跟调用 sws_getContextflags 有关,需要将 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);

  • 分配内存空间给用于存储原始数据的 AVFrame,指向原始帧数据;并且分配内存空间给用于存放视频解码前数据的 AVPacket

auto frame = av_frame_alloc();

auto packet = av_packet_alloc();

  • 从视频码流中循环读取压缩帧数据,然后开始解码;

ret = av_read_frame(av_format_context, packet);

if (packet->size) {

Decode(codec_ctx, packet, frame, stream, lock, data_convert_context, rgba_frame);

}

  • Decode() 函数中将装有原生压缩数据的 packet 作为输入发送给解码器;

/* send the packet with the compressed data to the decoder */

ret = avcodec_send_packet(codec_ctx, pkt);

  • 解码器返回解码后的帧数据到指定的 frame 上,后续可对已解码 framepts 换算为时间戳,按时间轴的显示顺序逐帧绘制到播放的画面上;

while (ret >= 0 && !is_stop_) {

// 返回解码后的数据到 frame

ret = avcodec_receive_frame(codec_ctx, frame);

if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {

return;

} else if (ret < 0) {

return;

}

// 拿到当前解码后的 frame,对其 pts 换算成时间戳,以便于跟传入的指定时间戳进行比

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;

// ······

// 图片数据格式转换

// ······

// 把转换后的数据绘制到屏幕上

}

av_packet_unref(pkt);

}

  • 绘制画面之前,要进行图片数据格式的转换,这里就要用到前面获取到的 SwsContext

// 图片数据格式转换

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;

}

  • 因为是上屏绘制,所以用到了 ANativeWindowANativeWindow_Buffer。在绘制画面之前,需要使用锁定窗口的下一个绘图 surface 以进行绘制,然后将要显示的帧数据写入到缓冲区中,最后解锁窗口的绘图 surface,将缓冲区的数据发布到屏幕显示上;

// 播放

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 一行的像素长度可能不一致

// 需要转换好 否则可能花屏

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_);

}

  • 以上就是主要的解码过程。除此之外,因为 C++ 使用资源和内存空间时需要自行释放,所以解码结束后还需要调用释放的接口释放资源,以免造成内存泄漏。

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 简单应用

为了更好地理解视频解码的过程,这里封装一个视频解码器 VideoDecoder ,解码器初步会有以下几个函数:

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

void Prepare(ANativeWindow* window);

bool DecodeFrame(long time_ms);

void Release();

在这个视频解码器中,输入指定时间戳后会返回解码的这一帧数据。其中较为重要的是 DecodeFrame(long time_ms) 函数,它可以由使用者自行调用,传入指定帧的时间戳,进而解码对应的帧数据。此外,可以增加同步锁以实现解码线程和使用线程分离。

2.3.1 加入同步锁实现视频播放

若只要对视频进行解码,是不需要使用同步等待的;

但若是要实现视频的播放,那么每解码绘制完一帧就需使用锁进行同步等待,这是因为播放视频时需要让解码和绘制分离、且按照一定的时间轴顺序和速度进行解码和绘制。

condition_.wait(lock);

在上层调用 DecodeFrame 函数传入解码的时间戳时唤醒同步锁,让解码绘制的循环继续执行。

bool VideoDecoder::DecodeFrame(long time_ms) {

// ······

time_ms_ = time_ms;

condition_.notify_all();

return true;

}

2.3.2 播放时加入 seek_frame

在正常播放情况下,视频是一帧一帧逐帧解码播放;但在拖动进度条到达指定的 seek 点的情况下,如果还是从头到尾逐帧解码到 seek 点的话,效率可能不太高。这时候就需要在一定规则内对 seek 点的时间戳做检查,符合条件的直接 seek 到指定的时间戳。

FFmpeg 中的 av_seek_frame
  • av_seek_frame 可以定位到关键帧和非关键帧,这取决于选择的 flag 值。因为视频的解码需要依赖关键帧,所以一般我们需要定位到关键帧;

int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp,

int flags);

  • av_seek_frame 中的 flag 是用来指定寻找的 I 帧和传入的时间戳之间的位置关系。当要 seek 已过去的时间戳时,时间戳不一定会刚好处在 I 帧的位置,但因为解码需要依赖 I 帧,所以需要先找到此时间戳附近一个的 I 帧,此时 flag 就表明要 seek 到当前时间戳的前一个 I 帧还是后一个 I 帧;

  • flag 有四个选项:

| flag 选项 | 描述 |

| — | — |

| AVSEEK_FLAG_BACKWARD | 第一个 Flag 是 seek 到请求的时间戳之前最近的关键帧。通常情况下,seek 以 ms 为单位,若指定的 ms 时间戳刚好不是关键帧(大几率),会自动往回 seek 到最近的关键帧。虽然这种 flag 定位并不是非常精确,但能够较好地处理掉马赛克的问题,因为 BACKWARD 的方式会向回查找关键帧处,定位到关键帧处。 |

| AVSEEK_FLAG_BYTE | 第二个 Flag 是 seek 到文件中对应的位置(字节表示),和 AVSEEK_FLAG_FRAME 完全一致,但查找算法不同。 |

| AVSEEK_FLAG_ANY | 第三个 Flag 是可以 seek 到任意帧,不一定是关键帧,因此使用时可能出现花屏(马赛克),但进度和手滑完全一致。 |

| AVSEEK_FLAG_FRAME | 第四个 Flag 是 seek 的时间戳对应 frame 序号,可以理解为向后找到最近的关键帧,与 BACKWARD 的方向是相反的。 |

  • flag 可能同时包含以上的多个值。比如 AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_BYTE

  • FRAMEBACKWARD 是按帧之间的间隔推算出 seek 的目标位置,适合快进快退;BYTE 则适合大幅度滑动。

seek 的场景
  • 解码时传入的时间戳若是往前进的方向,并且超过上一帧时间戳有一定距离就需要 seek,这里的“一定距离”是通过多次实验估算所得,并非都是以下代码中使用的 1000ms;

  • 如果是往后退的方向且小于上一次解码时间戳,但与上一次解码时间戳的距离比较大(比如已超过 50ms),就要 seek 到上一个关键帧;

  • 使用 bool 变量 is_seeking_ 是为了防止其他干扰当前 seeking 的操作,目的是控制当前只有一个 seek 操作在进行。

if (!is_seeking_ && (time_ms_ > last_decode_time_ms_ + 1000 ||

time_ms_ < last_decode_time_ms_ - 50)) {

is_seeking_ = true;

// seek 时传入的是指定帧带有 time_base 的时间戳,因此要用 times_ms 进行推算

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 的逻辑

因为在解码前要检查是否 seek,所以要在 av_read_frame 函数(返回视频媒体流下一帧)之前插入 seek 的逻辑,符合 seek 条件时使用 av_seek_frame 到达指定 I 帧,接着 av_read_frame 后再继续解码到目的时间戳的位置。

// 是否进行 seek 的逻辑写在这

// 接下来是读取视频流的下一帧

int ret = av_read_frame(av_format_context, packet);

2.4 解码过程中的细节

2.4.1 DecodeFrame 时 seek 的条件

使用 av_seek_frame 函数时需要指定正确的 flag,并且还要约定进行 seek 操作时的条件,否则视频可能会出现花屏(马赛克)。

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 减少解码的次数

在视频解码时,在有些条件下是可以不用对传入时间戳的帧数据进行解码的。比如:

  1. 当前解码时间戳若是前进方向并且与上一次的解码时间戳相同或者与当前正在解码的时间戳相同,则不需要进行解码;

  2. 当前解码时间戳若不大于上一次的解码时间戳并且与上一次的解码时间戳之间的距离相差较小(比如未超过 50ms),则不需要进行解码。

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;

}

有了以上这些条件的约束后,会减少一些不必要的解码操作。

2.4.3 使用 AVFrame 的 pts
  1. AVPacket 存储解码前的数据(编码数据:H264/AAC 等),保存的是解封装之后、解码前的数据,仍然是压缩数据;AVFrame 存储解码后的数据(像素数据:YUV/RGB/PCM 等);

  2. AVPacketptsAVFramepts 意义存在差异。前者表示这个解压包何时显示,后者表示帧数据何时显示;

// 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;

  1. 是否将当前解码的帧数据绘制到画面上,取决于传入到解码时间戳与当前解码器返回的已解码帧的时间戳的比较结果。这里不可使用 AVPacketpts,它很可能不是一个递增的时间戳;

  2. 需要进行画面绘制的前提是:当传入指定的解码时间戳不大于当前已解码 frame 的 pts 换算后的时间戳时进行画面绘制。

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 解码最后一帧时视频已经没有数据

使用 av_read_frame(av_format_context, packet)返回视频媒体流下一帧到 AVPacket 中。如果函数返回的 int 值是 0 则是 Success,如果小于 0 则是 Error 或者 EOF

因此如果在播放视频时返回的是小于 0 的值,调用 avcodec_flush_buffers 函数重置解码器的状态,flush 缓冲区中的内容,然后再 seek 到当前传入的时间戳处,完成解码后的回调,再让同步锁进行等待。

// 读取码流中的音频若干帧或者视频一帧,

// 这里是读取视频一帧(完整的一帧),获取的是一帧视频的压缩数据,接下来才能对其进行解码

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)”);

// 防止解最后一帧时视频已经没有数据

on_decode_frame_(last_decode_time_ms_);

condition_.wait(lock);

}

2.5 上层封装解码器 VideoDecoder

如果要在上层封装一个 VideoDecoder,只需要将 C++ 层 VideoDecoder 的接口暴露在 native-lib.cpp 中,然后上层通过 JNI 的方式调用 C++ 的接口。

比如上层要传入指定的解码时间戳进行解码时,写一个 deocodeFrame 方法,然后把时间戳传到 C++ 层的 nativeDecodeFrame 进行解码,而 nativeDecodeFrame 这个方法的实现就写在 native-lib.cpp 中。

// FFmpegVideoDecoder.kt

class FFmpegVideoDecoder(

path: String,

val onDecodeFrame: (timestamp: Long, texture: SurfaceTexture, needRender: Boolean) -> Unit

){

// 抽第 timeMs 帧,根据 sync 是否同步等待

fun decodeFrame(timeMS: Long, sync: Boolean = false) {

// 若当前不需要抽帧时不进行等待

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 中调用 C++ 层 VideoDecoder 的接口 DecodeFrame ,这样就通过 JNI 的方式建立起了上层和 C++ 底层之间的联系

// 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);

}

三、心得


技术经验

  • FFmpeg 编译后与 Android 结合起来实现视频的解码播放,便捷性很高。

  • 由于是用 C++ 层实现具体的解码流程,会有学习难度,最好有一定的 C++ 基础。

四、附录


C++ 封装的 VideoDecoder

  • VideoDecoder.h

#include <jni.h>

#include 

#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 

/*

  • VideoDecoder 可用于解码某个音视频文件(比如.mp4)中视频媒体流的数据。

  • Java 层传入指定文件的路径后,可以按一定 fps 循环传入指定的时间戳进行解码(抽帧),这一实现由 C++ 提供的 DecodeFrame 来完成。

* 在每次解码结束时,将解码某一帧的时间戳回调给上层的解码器,以供其他操作使用。

*/

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 用于将抽取指定帧的时间戳回调给上层解码器,以供上层解码器进行其他操作。

std::function<void(long timestamp)> on_decode_frame_ = nullptr;

bool is_stop_ = false;

// 会与在循环同步时用的锁 “std::unique_lockstd::mutex” 配合使用

std::mutex work_queue_mtx;

// 真正在进行同步等待和唤醒的属性

std::condition_variable condition_;

// 解码器真正进行解码的函数

void Decode(AVCodecContext* codec_ctx, AVPacket* pkt, AVFrame* frame, AVStream* stream,

std::unique_lockstd::mutex& lock, SwsContext* sws_context, AVFrame* pFrame);

public:

// 新建解码器时要传入媒体文件路径和一个解码后的回调 on_decode_frame。

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

// 在 JNI 层将上层传入的 Surface 包装后新建一个 ANativeWindow 传入,在后面解码后绘制帧数据时需要用到

void Prepare(ANativeWindow* window);

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

都说三年是程序员的一个坎,能否晋升或者提高自己的核心竞争力,这几年就十分关键。

技术发展的这么快,从哪些方面开始学习,才能达到高级工程师水平,最后进阶到Android架构师/技术专家?我总结了这 5大块;

我搜集整理过这几年阿里,以及腾讯,字节跳动,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 PDF(实际上比预期多花了不少精力),包含知识脉络 + 分支细节。

Java语言与原理;
大厂,小厂。Android面试先看你熟不熟悉Java语言

高级UI与自定义view;
自定义view,Android开发的基本功。

性能调优;
数据结构算法,设计模式。都是这里面的关键基础和重点需要熟练的。

NDK开发;
未来的方向,高薪必会。

前沿技术;
组件化,热升级,热修复,框架设计

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

我在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多

当然,想要深入学习并掌握这些能力,并不简单。关于如何学习,做程序员这一行什么工作强度大家都懂,但是不管工作多忙,每周也要雷打不动的抽出 2 小时用来学习。

不出半年,你就能看出变化!

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

朋友,同时减轻大家的负担。**

[外链图片转存中…(img-fKFMCID9-1712337817473)]

[外链图片转存中…(img-SmsPU8Qc-1712337817473)]

[外链图片转存中…(img-z56PFfg2-1712337817474)]

[外链图片转存中…(img-SdF4LULF-1712337817474)]

[外链图片转存中…(img-erB9Jgnt-1712337817474)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

都说三年是程序员的一个坎,能否晋升或者提高自己的核心竞争力,这几年就十分关键。

技术发展的这么快,从哪些方面开始学习,才能达到高级工程师水平,最后进阶到Android架构师/技术专家?我总结了这 5大块;

我搜集整理过这几年阿里,以及腾讯,字节跳动,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 PDF(实际上比预期多花了不少精力),包含知识脉络 + 分支细节。

[外链图片转存中…(img-D9vbuwVb-1712337817475)]

Java语言与原理;
大厂,小厂。Android面试先看你熟不熟悉Java语言

[外链图片转存中…(img-8dDhOl70-1712337817475)]

高级UI与自定义view;
自定义view,Android开发的基本功。

[外链图片转存中…(img-h0xezFzM-1712337817476)]

性能调优;
数据结构算法,设计模式。都是这里面的关键基础和重点需要熟练的。

[外链图片转存中…(img-fgtngkzZ-1712337817476)]

NDK开发;
未来的方向,高薪必会。

[外链图片转存中…(img-37blIbXg-1712337817476)]

前沿技术;
组件化,热升级,热修复,框架设计

[外链图片转存中…(img-mgneYD4T-1712337817477)]

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

我在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多

当然,想要深入学习并掌握这些能力,并不简单。关于如何学习,做程序员这一行什么工作强度大家都懂,但是不管工作多忙,每周也要雷打不动的抽出 2 小时用来学习。

不出半年,你就能看出变化!

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值