概述

本文首先以 FFmpeg 视频解码为主题,主要介绍了 ffmpeg 进行解码视频时的主要流程、基本原理;其次,文章还讲述了与 FFmpeg 视频解码有关的简单应用,包括如何在原有的 FFmpeg 视频解码的基础上按照一定时间轴顺序播放视频、如何在播放视频时加入 seek 的逻辑;除此之外,文章重点介绍了解码视频时可能容易遗漏的细节,最后是简单地阐述了下如何封装一个具有基本的视频解码功能的 VideoDecoder。

前言FFmpeg

FFmpeg 是一套可以用来录制、转换数字音频、视频,并能将其转化为流的开源计算机程序,它可生成用于处理和操作多媒体数据的库,其中包含了先进的音视频解码库 libavcodec 和音视频格式转换库 libavformat。

FFmpeg 六大常用功能模块视频解码基础入门

android如何集成ffmpeg(一文读懂AndroidFFmpeg)(1)

android如何集成ffmpeg(一文读懂AndroidFFmpeg)(2)

一、 引入 FFmpeg 前的准备工作1.1 FFmpeg 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)' # ······

# 清空上次的编译 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

android如何集成ffmpeg(一文读懂AndroidFFmpeg)(3)

#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 "

1.2 在 Android 中引入 FFmpeg 的 so 库

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") } } }

#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()); }

# 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 ${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相关的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 解码视频的原理和细节2.1 主要流程

android如何集成ffmpeg(一文读懂AndroidFFmpeg)(4)

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 视频解码的整体思路

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

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

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

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

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

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

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

// 图片数据格式转换 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 { // 将图像绘制到界面上 // 注意 : 这里 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_); }

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

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

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 的方向是相反的。

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. AVPacket 的 pts 和 AVFrame 的 pts 意义存在差异。前者表示这个解压包何时显示,后者表示帧数据何时显示;

// 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. 是否将当前解码的帧数据绘制到画面上,取决于传入到解码时间戳与当前解码器返回的已解码帧的时间戳的比较结果。这里不可使用 AVPacket 的 pts,它很可能不是一个递增的时间戳;
  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); }

三、心得

技术经验

四、附录

C 封装的 VideoDecoder

#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 可用于解码某个音视频文件(比如.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_lock<std::mutex>” 配合使用 std::mutex work_queue_mtx; // 真正在进行同步等待和唤醒的属性 std::condition_variable condition_; // 解码器真正进行解码的函数 void Decode(AVCodecContext* codec_ctx, AVPacket* pkt, AVFrame* frame, AVStream* stream, std::unique_lock<std::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); // 抽取指定时间戳的视频帧,可由上层调用 bool DecodeFrame(long time_ms); // 释放解码器资源 void Release(); // 获取当前系统毫秒时间 static int64_t GetCurrentMilliTime(void); }; }

#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_) { // 对于frame, avcodec_receive_frame内部每次都先调用 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); // 换算当前解码的frame时间戳 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; // 数据格式转换 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中的所有空间数据,清理完毕后进行初始化操作,并且将 data 与 size 置为0,方便下次调用。 // 释放 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 ) { // 找到视频媒体流的下标 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]; // 找到已注册的解码器 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); // 分配内存空间给输出 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); // 通过设置宽高限制缓冲区中的像素数量,而非屏幕的物理显示尺寸。 // 如果缓冲区与物理屏幕的显示尺寸不相符,则实际显示可能会是拉伸,或者被压缩的图像 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_); // 传进去的是指定帧带有 time_base 的时间戳,所以是要将原来的 times_ms 按照上面获取时的计算方式反推算出时间戳 av_seek_frame(av_format_context, video_stream_index, time_ms_ * stream->time_base.den / 1000, AVSEEK_FLAG_BACKWARD); } // 读取视频一帧(完整的一帧),获取的是一帧视频的压缩数据,接下来才能对其进行解码 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); } 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(); } /** * 获取当前的毫秒级时间 */ int64_t VideoDecoder::GetCurrentMilliTime(void) { struct timeval tv{}; gettimeofday(&tv, nullptr); return tv.tv_sec * 1000.0 tv.tv_usec / 1000.0; } }

加入我们

我们是字节跳动影像团队,目前研发包括剪映、CapCut、轻颜、醒图、Faceu 在内的多款产品,业务覆盖多元化影像创作场景,截止 2021 年 6 月,剪映、轻颜相机、CapCut 等多次登顶国内外 APP Store 免费应用榜第一,并继续保持高速增长。加入我们,一起打造全球最受用户欢迎的影像创作产品。

社招投递链接:https://job.toutiao.com/s/NFYMcaq

校招内推码:5A38FTT

校招投递链接:https://jobs.bytedance.com/campus/position/7062599539027921189/detail?referral_code=5A38FTT

招贤纳士-字节跳动互娱研发影像团队:https://bytedance.feishu.cn/docx/doxcnMxgSioztbDuQqZ3eWDAvMc

第四期字节跳动技术沙龙

聚焦《字节云数据库架构设计与实战》

正在火热报名中!

4 位字节工程师倾情分享

3 小时 技术盛宴“码”力全开

扫描下方二维码免费报名

android如何集成ffmpeg(一文读懂AndroidFFmpeg)(5)

,