t707拆机(索尼爱立信t707拆机)-9游会

摘要

t707拆机(索尼爱立信t707拆机)插图

本文首先以ffmpeg视频解码为主题,主要介绍了ffmpeg视频解码的主要流程和基本原理。其次,文章还描述了与ffmpeg视频解码相关的简单应用,包括如何在原有ffmpeg视频解码的基础上,按照一定的时间轴顺序播放视频,以及如何在播放视频时添加seek逻辑;另外,文章重点介绍了解码视频时可能容易遗漏的细节。最后简单说明如何封装一个具有基本视频解码功能的视频解码器。

序ffmpeg

ffmpeg是一个开源的计算机程序,可用于记录、转换数字音频和视频,并将其转换为流。可以生成处理和操作多媒体数据的库,包括高级音视频解码库libcodec和音视频格式转换库libformat。

ffmpeg的六个常用功能模块libformat:多媒体文件或协议的封装和解封装库,如 mp4、flv 等文件封装格式,rtmp、rtsp 等网络协议封装格式;libcodec:音视频解码核心库;libfilter:音视频、字幕滤镜库;libswscale:图像格式转换库;libswresample:音频重采样库;libutil:工具库视频解码基础知识介绍解复用(demux):解复用也可叫解封装。这里有一个概念叫封装格式,封装格式指的是音视频的组合格式,常见的有 mp4、flv、mkv 等。通俗来讲,封装是将音频流、视频流、字幕流以及其他附件按一定规则组合成一个封装的产物。而解封装起着与封装相反的作用,将一个流媒体文件拆解成音频数据和视频数据等。此时拆分后数据是经过压缩编码的,常见的视频压缩数据格式有 h264。

解码(decode):简单来说,就是对压缩的编码数据解压成原始的视频像素数据,常用的原始视频像素数据格式有 yuv。

色彩空间转换(color space convert):通常对于图像显示器来说,它是通过 rgb 模型来显示图像的,但在传输图像数据时使用 yuv 模型可以节省带宽。因此在显示图像时就需要将 yuv 像素格式的数据转换成 rgb 的像素格式后再进行渲染。渲染(render):将前面已经解码和进行色彩空间转换的每一个视频帧的数据发送给显卡以绘制在屏幕画面上。一、引入ffmpeg前的准备工作1.1 ffmpeg so库编译在 ffmpeg 9游会官网下载源码库并解压;下载 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.toolchain=$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将ffmpeg so库引入androidndk 环境、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(sedinstancestate: bundle?) { super.oncreate(sedinstancestate) 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 接口文件,ja 层中声明的 external 方法在这里得到实现;#include #include extern "c" jniexport jstring jnicallja_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: 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( util shared imported )set_target_properties( util properties imported_location ${ffmpeg_lib_dir}/libutil.so )add_library( swresample shared imported )set_target_properties( swresample properties imported_location ${ffmpeg_lib_dir}/libswresample.so )add_library( codec shared imported )set_target_properties( codec properties imported_location ${ffmpeg_lib_dir}/libcodec.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 上 util swresample codec -landroid # links the target library to the log library # included in the ndk. ${log-lib})以上的操作就将 ffmpeg 引入 android 项目。二、ffmpeg解码视频的原理和细节2.1主要流程

2.2基本原则2.2.1常用的ffmpeg接口// 1 分配 formatcontextformat_alloc_context();// 2 打开文件输入流format_open_input(formatcontext **ps, const char *url, const inputformat *fmt, dictionary **options);// 3 提取输入文件中的数据流信息format_find_stream_info(formatcontext *ic, dictionary **options);// 4 分配编解码上下文codec_alloc_context3(const codec *codec);// 5 基于与数据流相关的编解码参数来填充编解码器上下文codec_parameters_to_context(codeccontext *codec, const codecparameters *par);// 6 查找对应已注册的编解码器codec_find_decoder(enum codecid id);// 7 打开编解码器codec_open2(codeccontext *ctx, const codec *codec, dictionary **options);// 8 不停地从码流中提取压缩帧数据,获取的是一帧视频的压缩数据_read_frame(formatcontext *s, packet *pkt);// 9 发送原生的压缩数据输入到解码器(compressed data)codec_send_packet(codeccontext *ctx, const packet *pkt);// 10 接收解码器输出的解码数据codec_receive_frame(codeccontext *ctx, frame *frame);2.2.2视频解码的整体思路首先要注册 libformat 并且注册所有的编解码器、复用/解复用组、协议等。它是所有基于 ffmpeg 的应用程序中第一个被调用的函数, 只有调用了该函数,才能正常使用 ffmpeg 的各项功能。另外,在最新版本的 ffmpeg 中目前已经可以不用加入这行代码;_register_all();打开视频文件,提取文件中的数据流信息;auto _format_context = format_alloc_context();format_open_input(&_format_context, path_.c_str(), nullptr, nullptr);format_find_stream_info(_format_context, nullptr);然后获取视频媒体流的下标,才能找到文件中的视频媒体流;int video_stream_index = -1;for (int i = 0; i < _format_context->nb_streams; i ) { // 匹配找到视频媒体流的下标, if (_format_context->streams[i]->codecpar->codec_type == media_type_video) { video_stream_index = i; logd(tag, "find video stream index = %d", video_stream_index); break; }}获取视频媒体流、获取解码器上下文、获取解码器上下文、配置解码器上下文的参数值、打开解码器;// 获取视频媒体流auto stream = _format_context->streams[video_stream_index];// 找到已注册的解码器auto codec = codec_find_decoder(stream->codecpar->codec_id);// 获取解码器上下文codeccontext* codec_ctx = codec_alloc_context3(codec);// 将视频媒体流的参数配置到解码器上下文auto ret = codec_parameters_to_context(codec_ctx, stream->codecpar);if (ret >= 0) { // 打开解码器 codec_open2(codec_ctx, codec, nullptr); // ······}通过指定像素格式、图像宽、图像高来计算所需缓冲区需要的内存大小,分配设置缓冲区;并且由于是上屏绘制,因此我们需要用到 anativewindow,使用 anativewindow_setbuffersgeometry 设置此绘制窗口的属性;video_width_ = codec_ctx->width;video_height_ = codec_ctx->height;int buffer_size = _image_get_buffer_size(_pix_fmt_rgba, video_width_, video_height_, 1);// 输出 bufferout_buffer_ = (uint8_t*) _malloc(buffer_size * sizeof(uint8_t));// 通过设置宽高来限制缓冲区中的像素数量,而非显示屏幕的尺寸。// 如果缓冲区与显示的屏幕尺寸不相符,则实际显示的可能会是拉伸,或者被压缩的图像int result = anativewindow_setbuffersgeometry(native_window_, video_width_, video_height_, window_format_rgba_8888);分配内存空间给像素格式为 rgba 的 frame,用于存放转换成 rgba 后的帧数据;设置 rgba_frame 缓冲区,使其与 out_buffer_ 相关联;auto rgba_frame = _frame_alloc();_image_fill_arrays(rgba_frame->data, rgba_frame->linesize, out_buffer_, _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_, _pix_fmt_rgba, sws_bicubic, nullptr, nullptr, nullptr);分配内存空间给用于存储原始数据的 frame,指向原始帧数据;并且分配内存空间给用于存放视频解码前数据的 packet;auto frame = _frame_alloc();auto packet = _packet_alloc();从视频码流中循环读取压缩帧数据,然后开始解码;ret = _read_frame(_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 = codec_send_packet(codec_ctx, pkt);解码器返回解码后的帧数据到指定的 frame 上,后续可对已解码 frame 的 pts 换算为时间戳,按时间轴的显示顺序逐帧绘制到播放的画面上;while (ret >= 0 && !is_stop_) { // 返回解码后的数据到 frame ret = codec_receive_frame(codec_ctx, frame); if (ret == error(eagain) || ret == error_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; // ······ // 图片数据格式转换 // ······ // 把转换后的数据绘制到屏幕上 } _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);_free(out_buffer_);_frame_free(&rgba_frame);_frame_free(&frame);_packet_free(&packet);codec_close(codec_ctx);codec_free_context(&codec_ctx);format_close_input(&_format_context);format_free_context(_format_context);anativewindow_release(native_window_);2.3简单应用

为了更好的理解视频解码的过程,这里打包了一个videodecoder,videodecoder。最初,解码器将具有以下功能:

videodecoder(const char* path, std::function 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。

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

ffmpeg中的_seek_frame_seek_frame 可以定位到关键帧和非关键帧,这取决于选择的 flag 值。因为视频的解码需要依赖关键帧,所以一般我们需要定位到关键帧;int _seek_frame(formatcontext *s, int stream_index, int64_t timestamp, int flags);_seek_frame 中的 flag 是用来指定寻找的 i 帧和传入的时间戳之间的位置关系。当要 seek 已过去的时间戳时,时间戳不一定会刚好处在 i 帧的位置,但因为解码需要依赖 i 帧,所以需要先找到此时间戳附近一个的 i 帧,此时 flag 就表明要 seek 到当前时间戳的前一个 i 帧还是后一个 i 帧;flag 有四个选项:

标志选项

形容

seek_flag_backward

第一个标志是从查找到请求的时间戳最近的关键帧。一般seek是以ms为单位的,如果指定的ms时间戳恰好不是关键帧(大概率),它会自动寻回到最近的关键帧。虽然这个标志定位不是很准确,但是可以更好的处理马赛克问题,因为向后的方法会把关键帧找回来,定位在关键帧上。

seek_flag_byte

第二个标志是seek到文件中相应的位置(字节表示),和seek_flag_frame完全一样,只是搜索算法不同。

seek_flag_any

第三个标志可以在任何一帧中找到,不一定是关键帧,所以使用时可能会出现花屏(马赛克),但进度和手滑完全一样。

seek_flag_frame

第四个标志是寻道时间戳对应的帧序列号,可以理解为向后寻找最近的关键帧,与向后的方向相反。

flag 可能同时包含以上的多个值。比如 seek_flag_backward | seek_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_); _seek_frame(_format_context, video_stream_index, time_ms_ * stream->time_base.den / 1000, seek_flag_backward);}插入seek的逻辑

解码前需要检查是否是seek,所以seek的逻辑要插在_read_frame函数之前(返回视频媒体流的下一帧)。当满足seek条件时,使用_seek_frame到达指定的i帧,然后使用_read_frame继续解码到目的时间戳的位置。

// 是否进行 seek 的逻辑写在这// 接下来是读取视频流的下一帧int ret = _read_frame(_format_context, packet);2.4解码过程中的细节2.4.1解码帧时的搜索条件

在使用_seek_frame函数时,需要指定正确的标志,还要规定查找操作的条件,否则视频可能会出现马赛克。

if (!is_seeking_ && (time_ms_ > last_decode_time_ms_ 1000 || time_ms_ < last_decode_time_ms_ – 50)) { is_seeking_ = true; _seek_frame(···,···,···,seek_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使用frame的ptpacket 存储解码前的数据(编码数据:h264/aac 等),保存的是解封装之后、解码前的数据,仍然是压缩数据;frame 存储解码后的数据(像素数据:yuv/rgb/pcm 等);packet 的 pts 和 frame 的 pts 意义存在差异。前者表示这个解压包何时显示,后者表示帧数据何时显示;// packet 的 pts /** * presentation timestamp in stream->time_base units; the time at which * the decompressed packet will be presented to the user. * can be _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 packet. */ int64_t pts; // frame 的 pts /** * presentation timestamp in time_base units (time when frame should be shown to user). */ int64_t pts;是否将当前解码的帧数据绘制到画面上,取决于传入到解码时间戳与当前解码器返回的已解码帧的时间戳的比较结果。这里不可使用 packet 的 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最后一帧解码时,视频没有数据。

使用 _ read _ frame( _ format _ context,packet)将视频媒体流的下一帧返回给packet。如果函数返回的int值为0,则为成功;如果小于0,则为错误或eof。

因此,如果播放视频时返回的值小于0,则调用codec_flush_buffers函数重置解码器的状态和flush缓冲区的内容,然后搜索到当前传入的时间戳,解码后完成回调,然后让同步锁等待。

// 读取码流中的音频若干帧或者视频一帧,// 这里是读取视频一帧(完整的一帧),获取的是一帧视频的压缩数据,接下来才能对其进行解码ret = _read_frame(_format_context, packet);if (ret < 0) { codec_flush_buffers(codec_ctx); _seek_frame(_format_context, video_stream_index, time_ms_ * stream->time_base.den / 1000, seek_flag_backward); logd(tag, "ret < 0, condition_.wait(lock)"); // 防止解最后一帧时视频已经没有数据 on_decode_frame_(last_decode_time_ms_); condition_.wait(lock);}2.5上层包解码器视频解码器

如果想在上层封装一个videodecoder,只需要在native-lib.cpp中暴露c 层videodecoder的接口,然后上层通过jni调用c 的接口即可。

比如上层要传入指定的解码时间戳进行解码,写一个deocodeframe方法,然后把时间戳传到c 层的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 oolean jnicallja_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.h#include #include #include #include #include extern "c" {#include #include #include #include }#include /* * videodecoder 可用于解码某个音视频文件(比如.mp4)中视频媒体流的数据。 * ja 层传入指定文件的路径后,可以按一定 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 on_decode_frame_ = nullptr; bool is_stop_ = false; // 会与在循环同步时用的锁 “std::unique_lock” 配合使用 std::mutex work_queue_mtx; // 真正在进行同步等待和唤醒的属性 std::condition_variable condition_; // 解码器真正进行解码的函数 void decode(codeccontext* codec_ctx, packet* pkt, frame* frame, stream* stream, std::unique_lock& lock, swscontext* sws_context, frame* pframe);public: // 新建解码器时要传入媒体文件路径和一个解码后的回调 on_decode_frame。 videodecoder(const char* path, std::function 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 #include extern "c" {#include }#define tag "videodecoder"namespace codec {videodecoder::videodecoder(const char* path, std::function on_decode_frame) : on_decode_frame_(std::move(on_decode_frame)) { path_ = std::string(path);}void videodecoder::decode(codeccontext* codec_ctx, packet* pkt, frame* frame, stream* stream, std::unique_lock& lock, swscontext* sws_context, frame* rgba_frame) { int ret; /* send the packet with the compressed data to the decoder */ ret = codec_send_packet(codec_ctx, pkt); if (ret == error(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, codec_receive_frame内部每次都先调用 ret = codec_receive_frame(codec_ctx, frame); if (ret == error(eagain) || ret == error_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); } // 主要作用是清理packet中的所有空间数据,清理完毕后进行初始化操作,并且将 data 与 size 置为0,方便下次调用。 // 释放 packet 引用 _packet_unref(pkt); }}void videodecoder::prepare(anativewindow* window) { native_window_ = window; _register_all(); auto _format_context = format_alloc_context(); format_open_input(&_format_context, path_.c_str(), nullptr, nullptr); format_find_stream_info(_format_context, nullptr); int video_stream_index = -1; for (int i = 0; i < _format_context->nb_streams; i ) { // 找到视频媒体流的下标 if (_format_context->streams[i]->codecpar->codec_type == media_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 lock(work_queue_mtx); // 获取视频媒体流 auto stream = _format_context->streams[video_stream_index]; // 找到已注册的解码器 auto codec = codec_find_decoder(stream->codecpar->codec_id); // 获取解码器上下文 codeccontext* codec_ctx = codec_alloc_context3(codec); auto ret = codec_parameters_to_context(codec_ctx, stream->codecpar); if (ret >= 0) { // 打开 codec_open2(codec_ctx, codec, nullptr); // 解码器打开后才有宽高的值 video_width_ = codec_ctx->width; video_height_ = codec_ctx->height; frame* rgba_frame = _frame_alloc(); int buffer_size = _image_get_buffer_size(_pix_fmt_rgba, video_width_, video_height_, 1); // 分配内存空间给输出 buffer out_buffer_ = (uint8_t*) _malloc(buffer_size * sizeof(uint8_t)); _image_fill_arrays(rgba_frame->data, rgba_frame->linesize, out_buffer_, _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"); codec_close(codec_ctx); codec_free_context(&codec_ctx); _free(out_buffer_); break; } auto frame = _frame_alloc(); auto packet = _packet_alloc(); struct swscontext* data_convert_context = sws_getcontext( video_width_, video_height_, codec_ctx->pix_fmt, video_width_, video_height_, _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 按照上面获取时的计算方式反推算出时间戳 _seek_frame(_format_context, video_stream_index, time_ms_ * stream->time_base.den / 1000, seek_flag_backward); } // 读取视频一帧(完整的一帧),获取的是一帧视频的压缩数据,接下来才能对其进行解码 ret = _read_frame(_format_context, packet); if (ret < 0) { codec_flush_buffers(codec_ctx); _seek_frame(_format_context, video_stream_index, time_ms_ * stream->time_base.den / 1000, seek_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); _free(out_buffer_); _frame_free(&rgba_frame); _frame_free(&frame); _packet_free(&packet); } codec_close(codec_ctx); codec_free_context(&codec_ctx); } while (false); format_close_input(&_format_context); format_free_context(_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;}}加入j9九游会真人游戏第一品牌

我们是字节跳动的一个成像团队。目前,我们研发了多种产品,包括clipping、capcut、light face、waking、faceu,我们的业务涵盖了广泛的图像创作场景。到2021年6月,clipping,轻面相机,capcut等。多次荣登国内外app store免费应用榜首,并继续保持高速增长。加入j9九游会真人游戏第一品牌,创造世界上最受欢迎的图像创作产品。

招聘和交付环节:

学校推送代码:5a38ftt

校招投递链接: . byte dance . com/campus/position//detail?referral_code=5a38ftt

仙娜诗-字节跳动互娱r&d影像团队:

第四届字节跳动技术沙龙

专注于字节云数据库架构的设计与实践

热注册正在进行中!

4字节工程师分享感受。

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

扫描下方二维码免费注册。

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文链接:https://www.andon8.com/5227.html

网站地图