概述本文起首以 FFmpeg 视频解码为主题,次要介绍了 FFmpeg 停止解码视频时的次要流程、根本原理;其次,文章还讲述了与 FFmpeg 视频解码有关的简单利用,包罗若何在原有的 FFmpeg 视频解码的根底上根据必然时间轴挨次播放视频、若何在播放视频时加进 seek 的逻辑;除此之外,文章重点介绍领会码视频时可能随便遗漏的细节,最初是简单地论述了下若何封拆一个具有根本的视频解码功用的 VideoDecoder。
媒介FFmpegFFmpeg 是一套能够用来录造、转换数字音频、视频,并能将其转化为流的开源计算机法式,它可生成用于处置和操做多媒体数据的库,此中包罗了先辈的音视频解码库 libavcodec 和音视频格局转换库 libavformat。
FFmpeg 六大常用功用模块libavformat:多媒体文件或协议的封拆息争封拆库,如 mp4、flv 等文件封拆格局,rtmp、rtsp 等收集协议封拆格局;libavcodec:音视频解码核心库;libavfilter:音视频、字幕滤镜库;libswscale:图像格局转换库;libswresample:音频重摘样库;libavutil:东西库视频解码根底进门解复用(Demux):解复用也可喊解封拆。那里有一个概念喊封拆格局,封拆格局指的是音视频的组合格局,常见的有 mp4、flv、mkv 等。通俗来讲,封拆是将音频流、视频流、字幕流以及其他附件按必然规则组合成一个封拆的产品。而解封拆起着与封拆相反的感化,将一个流媒体文件拆解成音频数据和视频数据等。此时拆分后数据是颠末压缩编码的,常见的视频压缩数据格局有 h264。
解码(Decode):简单来说,就是对压缩的编码数据解压成原始的视频像素数据,常用的原始视频像素数据格局有 yuv。
色彩空间转换(Color Space Convert):凡是关于图像展现器来说,它是通过 RGB 模子来展现图像的,但在传输图像数据时利用 YUV 模子能够节约带宽。因而在展现图像时就需要将 yuv 像素格局的数据转换成 rgb 的像素格局后再停止衬着。衬着(Render):将前面已经解码和停止色彩空间转换的每一个视频帧的数据发送给显卡以绘造在屏幕画面上。一、 引进 FFmpeg 前的预备工做1.1 FFmpeg so 库编译在 FFmpeg 官网下载源码库并解压;下载 NDK 库并解压;设置装备摆设解压后的 FFmpeg 源码库目次中的 configure,修改高亮部门几个参数为以下的内容,次要目标是生成 Android 可利用的 名称-版本.so 文件的格局;# ······# build settingsSHFLAGS='-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 源码库目次下新建脚本文件 build_android_arm_v8a.sh,在文件中设置装备摆设 NDK 的途径,并输进下面其他的内容;# 清空前次的编译make clean# 那里先设置装备摆设你的 NDK 途径export NDK=/Users/bytedance/Library/Android/sdk/ndk/21.4.7075529TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/darwin-x86_64function 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 cleanmake -j16make installecho "============================ build android arm64-v8a success =========================="}# arm64-v8aARCH=arm64CPU=armv8-aAPI=21CC=$TOOLCHAIN/bin/aarch64-linux-android$API-clangCXX=$TOOLCHAIN/bin/aarch64-linux-android$API-clang++SYSROOT=$NDK/toolchains/llvm/prebuilt/darwin-x86_64/sysrootCROSS_PREFIX=$TOOLCHAIN/bin/aarch64-linux-android-PREFIX=$(pwd)/android/$CPUOPTIMIZE_CFLAGS="-march=$CPU"echo $CCbuild_android设置 NDK 文件夹中所有文件的权限 chmod 777 -R NDK;末端施行脚本 ./build_android_arm_v8a.sh,起头编译 FFmpeg。编译胜利后的文件会在 FFmpeg 下的 android 目次中,会呈现多个 .so 文件;
若要编译 arm-v7a,只需要拷贝修改以上的脚本为以下 build_android_arm_v7a.sh 的内容。#armv7-aARCH=armCPU=armv7-aAPI=21CC=$TOOLCHAIN/bin/armv7a-linux-androideabi$API-clangCXX=$TOOLCHAIN/bin/armv7a-linux-androideabi$API-clang++SYSROOT=$NDK/toolchains/llvm/prebuilt/darwin-x86_64/sysrootCROSS_PREFIX=$TOOLCHAIN/bin/arm-linux-androideabi-PREFIX=$(pwd)/android/$CPUOPTIMIZE_CFLAGS="-mfloat-abi=softfp -mfpu=vfp -marm -march=$CPU "1.2 在 Android 中引进 FFmpeg 的 so 库NDK 情况、CMake 构建东西、LLDB(C/C++ 代码调试东西);新建 C++ module,一般会生成以下几个重要的文件:CMakeLists.txt、native-lib.cpp、MainActivity;在 app/src/main/ 目次下,新建目次,并定名 jniLibs,那是 Android Studio 默认放置 so 动态库的目次;接着在 jniLibs 目次下,新建 arm64-v8a 目次,然后将编译好的 .so 文件粘贴至此目次下;然后再将编译时生成的 .h 头文件(FFmpeg 对外表露的接口)粘贴至 cpp 目次下的 include 中。以上的 .so 动态库目次和 .h 头文件目次城市在 CMakeLists.txt 中显式声明和链接进来;最上层的 MainActivity,在那里面加载 C/C++ 代码编译的库: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() } // 声明一个外部引用的办法,此办法和 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 stringextern "C" JNIEXPORT jstring JNICALLJava_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: 次要流程
2.2 根本原理2.2.1 常用的 ffmpeg 接口// 1 分配 AVFormatContextavformat_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);// 输出 bufferout_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_getContext 时 flags 有关,需要将 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 上,后续可对已解码 frame 的 pts 换算为时间戳,定时间轴的展现挨次逐帧绘造到播放的画面上;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;}因为是上屏绘造,所以用到了 ANativeWindow 和 ANativeWindow_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::functionvoid(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_frameav_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;FRAME 和 BACKWARD 是按帧之间的间隔推算出 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 削减解码的次数在视频解码时,在有些前提下是能够不消对传进时间戳的帧数据停止解码的。好比:
当前解码时间戳若是前进标的目的而且与上一次的解码时间戳不异或者与当前正在解码的时间戳不异,则不需要停止解码;当前解码时间戳若不大于上一次的解码时间戳而且与上一次的解码时间戳之间的间隔相差较小(好比未超越 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 的 ptsAVPacket 存储解码前的数据(编码数据:H264/AAC 等),保留的是解封拆之后、解码前的数据,仍然是压缩数据;AVFrame 存储解码后的数据(像素数据:YUV/RGB/PCM 等);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;能否将当前解码的帧数据绘造到画面上,取决于传进到解码时间戳与当前解码器返回的已解码帧的时间戳的比力成果。那里不成利用 AVPacket 的 pts,它很可能不是一个递增的时间戳;需要停止画面绘造的前提是:当传进指定的解码时间戳不大于当前已解码 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.ktclass 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.cppextern "C"JNIEXPORT jboolean JNICALLJava_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 mutex#include android/native_window.h#include android/native_window_jni.h#include time.hextern "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::functionvoid(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::functionvoid(long timestamp) on_decode_frame); // 在 JNI 层将上层传进的 Surface 包拆后新建一个 ANativeWindow 传进,在后面解码后绘造帧数据时需要用到 void Prepare(ANativeWindow* window); // 抽取指按时间戳的视频帧,可由上层挪用 bool DecodeFrame(long time_ms); // 释放解码器资本 void Release(); // 获取当前系统毫秒时间 static int64_t GetCurrentMilliTime(void);};}VideoDecoder.cpp#include "VideoDecoder.h"#include "../log/Logger.h"#include thread#include utilityextern "C" {#include libavutil/imgutils.h}#define TAG "VideoDecoder"namespace codec {VideoDecoder::VideoDecoder(const char* path, std::functionvoid(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_lockstd::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_lockstd::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 免费利用榜第一,并陆续连结高速增长。加进我们,一路打造全球最受用户欢送的影像创做产物。
社招送达链接:
校招内推码:5A38FTT
校招送达链接:
招贤纳士-字节跳动互娱研发影像团队:
第四期字节跳动手艺沙龙
聚焦《字节云数据库架构设想与实战》
正在炽热报名中!
4 位字节工程师倾情分享
3 小时+ 手艺盛宴“码”力全开
扫描下方二维码免费报名