当前位置: 首页 > news >正文

FFMPEG+ANativeWinodow渲染播放视频

前言

学习音视频开发,入门基本都得学FFMPEG,按照目前互联网上流传的学习路线,FFMPEG+ANativeWinodow渲染播放视频属于是第一关卡的Boss,简单但是关键。这几天写了个简单的demo,可以比较稳定进行渲染播放,便尝试进行记录一下。

编译

FFMPEG要想在安卓设备中进行使用,我们必须进行交叉编译,编译出设备可以使用的算法库。这部分的内容还是需要一个很详细的讲解,才能比较好理解,但是我还没有写,但是我后面可能会参考这篇文档进行编写。大家其实也可以直接看他这个,我到时候写可能就是简化一下,把应该改哪里说清楚,但是很多属性我应该不会详细进行讲解,因为我自己也不会。

(53 封私信 / 49 条消息) Android 大家有没有编译好的ffmpeg? - 知乎 (zhihu.com)

功能实现

算法库准备 

我们首先要把我们前面交叉编译好的算法库文件准备好,放入到工程中指定的位置。

CMAKE配置

我们放好了算法库之后,我们就要想办法将这些库引入到项目,让开发者可以直接通过接口调用库中的方法从而实现功能,我们这个项目是通过CMAKE进行管理,我们则需要进行CMAKE文件的配置。我这边先将我这个项目的所有CMAKE配置的内容放出来,大家可以凑合着看,我的注释也算挺多,不过还是建议可以先去看一下CMAKE的相关文档。

# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11")# 定义 jnilibs 变量,指向 JNI 库的目录。
set(jnilibs ${CMAKE_SOURCE_DIR}/../jniLibs)
# 定义 libname 变量,指定生成的库的名称为 learn-ffmpeg。
set(libname learn-ffmpeg)#向 CMake 环境中添加库的头文件路径
include_directories(include${CMAKE_SOURCE_DIR}/util
)#向 CMake 环境中添加库的路径,指定链接的库所在的目录为 jnilibs 下的当前 ABI 目录。link_directories(${jnilibs}/${ANDROID_ABI})#使用 GLOB 将所有 .cpp 文件匹配到 src-files 变量中。
file(GLOB src-files${CMAKE_SOURCE_DIR}/*.cpp)#添加一个共享库 learn-ffmpeg,将 src-files 中的源文件编译成共享库。
add_library( # Sets the name of the library.${libname}# Sets the library as a shared library.SHARED# Provides a relative path to your source file(s).${src-files})#定义需要链接的第三方库,包括 FFmpeg 相关的库。
set(third-party-libsavformatavcodecavfilterswresampleswscaleavutil)#定义需要链接的系统本地库,例如 Android、EGL、GLESv3、OpenSLES、log、m 和 z 等。
set(native-libsandroidEGLGLESv3OpenSLESlogmz)指定 learn-ffmpeg 库链接到的其它库,包括 log-lib、third-party-libs 和 native-libs
target_link_libraries( # Specifies the target library.${libname}# Links the target library to the log library# included in the NDK.${log-lib}${third-party-libs}${native-libs})

上面的代码段基本对每一行都有一个比较详细的注释,我这边再额外说一下下面这两个配置是为什么。

#向 CMake 环境中添加库的头文件路径
include_directories(
        include
        ${CMAKE_SOURCE_DIR}/util
)

#向 CMake 环境中添加库的路径,指定链接的库所在的目录为 jnilibs 下的当前 ABI 目录。

link_directories(
        ${jnilibs}/${ANDROID_ABI})

我们贴合实际编写native代码进行讲解,这两个语句是干什么的。

我们平时进行编写代码的时候是不是都会用到很多本地的库,然后我们会在代码中会通过通过#include去进行引入。然后我们就可以在代码中使用这个本地库的接口。那可能又有人会问了,为啥通过include引入头文件,就可以执行这些库的方法,头文件是没有方法体,那他们的方法体逻辑又在哪里呢?在项目中会有一个include文件夹,库的具体方法体逻辑就在这些里面。

那我们回过头,上面两句配置分别是做了什么事情呢,首先我们来看看第一句。 

#向 CMake 环境中添加库的头文件路径
include_directories(
        include
        ${CMAKE_SOURCE_DIR}/util
)

上面这个语句就是为了可以将我们编译的库的头文件引入到CMake环境的库中,这么操作的话,我们就可以直接使用#include去引入ffmpeg,从而可以在逻辑中使用ffmpeg库的接口。

但是光将头文件引入到CMake环境的库中是远远不够的,我们还需要引入方法体。这时候就是第二句的作用了: 

#向 CMake 环境中添加库的路径,指定链接的库所在的目录为 jnilibs 下的当前 ABI 目录。

link_directories(
        ${jnilibs}/${ANDROID_ABI})

通过这两句,我们就基本把ffmpeg引入到了CMAKE环境中。其实最后这句配置有一个很直观的理解。

布局文件 

这个demo主要还是为了梳理通FFMPEG实现播放器的流程,布局较为简单,基本就是通过一个surfaceview控件播放指定的视频。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".Demo2Activity"android:orientation="vertical"><Buttonandroid:id="@+id/selectmp4btn"android:text="选择MP4文件"android:layout_width="match_parent"android:layout_height="wrap_content"/><SurfaceViewandroid:id="@+id/nativesurfaceview"android:layout_width="match_parent"android:layout_height="match_parent"/></LinearLayout>
package com.example.learnffmpegapplication;import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.provider.Settings;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;import com.example.learnffmpegapplication.utils.FileChooseUtil;public class Demo2Activity extends AppCompatActivity implements SurfaceHolder.Callback, View.OnClickListener {private SurfaceView mNativeSurfaceView;private Button mSelectMp4Btn;private FFMediaPlayer mFFMediaPlayer;private String mVideoPath = "https://prod-streaming-video-msn-com.akamaized.net/aa5cb260-7dae-44d3-acad-3c7053983ffe/1b790558-39a2-4d2a-bcd7-61f075e87fdd.mp4";private static final int PERMISSION_REQUEST_CODE = 1001;private Surface ANativeWindowSurface;private boolean mIsFirstTimeEnter = true;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_demo2);mVideoPath = getFilesDir().getAbsolutePath() + "/byteflow/vr.mp4";initView();toRequestPermission();mIsFirstTimeEnter = true;}private void toRequestPermission() {requestStoragePermissions(this);}private void requestStoragePermissions(Context context) {String[] permissions;if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {permissions = new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE};} else {permissions = new String[]{Manifest.permission.READ_MEDIA_AUDIO, Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VIDEO};}ActivityCompat.requestPermissions((Activity) context,permissions,PERMISSION_REQUEST_CODE);}private void initView() {mNativeSurfaceView = findViewById(R.id.nativesurfaceview);SurfaceHolder holder = mNativeSurfaceView.getHolder();holder.addCallback(this);ANativeWindowSurface = holder.getSurface();mSelectMp4Btn = findViewById(R.id.selectmp4btn);mSelectMp4Btn.setOnClickListener(this);}@Overridepublic void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {super.onRequestPermissionsResult(requestCode, permissions, grantResults);if (requestCode == PERMISSION_REQUEST_CODE) {if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {// 在这里执行所需权限已授予后的操作} else {Toast.makeText(this, "需要授予外部存储访问权限才能选择MP4文件", Toast.LENGTH_SHORT).show();}}}@Overridepublic void surfaceCreated(@NonNull SurfaceHolder holder) {Log.d("yjs", "onSurfaceCreated");mFFMediaPlayer = new FFMediaPlayer();}@Overridepublic void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {Log.d("yjs", "onSurfaceChanged");Log.d("yjs", "mVideoPath:" + mVideoPath);Log.d("yjs", "mIsFirstTimeEnter:" + mIsFirstTimeEnter);new Thread(new Runnable() {@Overridepublic void run() {mFFMediaPlayer.startPlayingVideo(mVideoPath, ANativeWindowSurface);}}).start();}@Overridepublic void surfaceDestroyed(@NonNull SurfaceHolder holder) {Log.d("yjs", "onSurfaceDestroyed");}@Overrideprotected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {super.onActivityResult(requestCode, resultCode, data);if (resultCode == Activity.RESULT_OK) {Uri uri = data.getData();if ("file".equalsIgnoreCase(uri.getScheme())) {//使用第三方应用打开mVideoPath = uri.getPath();Log.d("yjs", "返回结果1: " + mVideoPath);return;}if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {//4.4以后mVideoPath = FileChooseUtil.getPath(this, uri);Log.d("yjs", "返回结果2: " + mVideoPath);} else {//4.4以下下系统调用方法mVideoPath = FileChooseUtil.getRealPathFromURI(uri);Log.d("yjs", "返回结果3: " + mVideoPath);}}}@Overridepublic void onClick(View v) {if (v.getId() == R.id.selectmp4btn) {Intent intent = new Intent(Intent.ACTION_GET_CONTENT);intent.setType("video/*");intent.addCategory(Intent.CATEGORY_OPENABLE);startActivityForResult(intent, 1);}}
}

 这段代码看着好像很长,但是基本都在授权,获取授权结果之类的代码逻辑,真正重要的代码逻辑不会超过五十行。

其实这个类的逻辑非常简单,集成SurfaceHolder以便可以快捷的重写控制surface生命周期的三个重要方法,在surface创建的时候声明FFMediaPlayer播放工具类。在surface发生改变的时候通过播放工具类进行视频播放。

@Overridepublic void surfaceCreated(@NonNull SurfaceHolder holder) {Log.d("yjs", "onSurfaceCreated");mFFMediaPlayer = new FFMediaPlayer();}@Overridepublic void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {Log.d("yjs", "onSurfaceChanged");Log.d("yjs", "mVideoPath:" + mVideoPath);Log.d("yjs", "mIsFirstTimeEnter:" + mIsFirstTimeEnter);new Thread(new Runnable() {@Overridepublic void run() {mFFMediaPlayer.startPlayingVideo(mVideoPath, ANativeWindowSurface);}}).start();}@Overridepublic void surfaceDestroyed(@NonNull SurfaceHolder holder) {Log.d("yjs", "onSurfaceDestroyed");}

FFMediaPlayer 

这个工具类其实也是相当于一个中间商的作用,进行调用JNI的Native代码。我们后续可以着重看一下Native层是如何实现功能。

package com.example.learnffmpegapplication;import android.view.Surface;public class FFMediaPlayer {static {System.loadLibrary("learn-ffmpeg");}public static String GetFFmpegVersion() {return native_GetFFmpegVersion();}public void startPlayingVideo(String videoPath, Surface surface){native_StartToPlayingVideo(videoPath,surface);}private static native String native_GetFFmpegVersion();private native void native_StartToPlayingVideo(String videoPath,Surface surface);
}

FFMPEG Native使用流程步骤 

其实FFMPEG播放视频的流程还是比较固定的,我们只需对这些流程有一个基本的认识,很简单就可以实现一个播放器的功能。

    /** 初始化网络 :*      默认状态下 , FFMPEG 是不允许联网的*      必须调用该函数 , 初始化网络后 FFMPEG 才能进行联网*/avformat_network_init();//0 . 注册组件//      如果是 4.x 之前的版本需要执行该步骤//      4.x 及之后的版本 , 就没有该步骤了//av_register_all();//1 . 打开音视频地址 ( 播放文件前 , 需要先将文件打开 )//      地址类型 : ① 文件类型 , ② 音视频流//  参数解析 ://      AVFormatContext **ps :  封装了文件格式相关信息的结构体 , 如视频宽高 , 音频采样率等信息 ;//                              该参数是 二级指针 , 意味着在方法中会修改该指针的指向 ,//                              该参数的实际作用是当做返回值用的//      const char *url :   视频资源地址, 文件地址 / 网络链接//  返回值说明 : 返回 0 , 代表打开成功 , 否则失败//              失败的情况 : 文件路径错误 , 网络错误//int avformat_open_input(AVFormatContext **ps, const char *url,//                          AVInputFormat *fmt, AVDictionary **options);formatContext = 0;int open_result = avformat_open_input(&formatContext, dataSource, 0, 0);//如果返回值不是 0 , 说明打开视频文件失败 , 需要将错误信息在 Java 层进行提示//  这里将错误码返回到 Java 层显示即可if(open_result != 0){__android_log_print(ANDROID_LOG_ERROR , "FFMPEG" , "打开媒体失败 : %s", av_err2str(open_result));callHelper->onError(pid, 0);}//2 . 查找媒体 地址 对应的音视频流 ( 给 AVFormatContext* 成员赋值 )//      方法原型 : int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);//      调用该方法后 , AVFormatContext 结构体的 nb_streams 元素就有值了 ,//      该值代表了音视频流 AVStream 个数int find_result = avformat_find_stream_info(formatContext, 0);//如果返回值 < 0 , 说明查找音视频流失败 , 需要将错误信息在 Java 层进行提示//  这里将错误码返回到 Java 层显示即可if(find_result < 0){__android_log_print(ANDROID_LOG_ERROR , "FFMPEG" , "查找媒体流失败 : %s", av_err2str(find_result));callHelper->onError(pid, 1);}//formatContext->nb_streams 是 音频流 / 视频流 个数 ;//  循环解析 视频流 / 音频流 , 一般是两个 , 一个视频流 , 一个音频流for(int i = 0; i < formatContext->nb_streams; i ++){//取出一个媒体流 ( 视频流 / 音频流 )AVStream *stream = formatContext->streams[i];}//获取音视频流的编码参数//解码这个媒体流的参数信息 , 包含 码率 , 宽度 , 高度 , 采样率 等参数信息AVCodecParameters *codecParameters = stream->codecpar;//查找编解码器//① 查找 当前流 使用的编码方式 , 进而查找编解码器 ( 可能失败 , 不支持的解码方式 )AVCodec *avCodec = avcodec_find_decoder(codecParameters->codec_id);//获取编解码器上下文AVCodecContext *avCodecContext = avcodec_alloc_context3(avCodec);//设置编解码器上下文参数//      int avcodec_parameters_to_context(AVCodecContext *codec,//              const AVCodecParameters *par);//      返回值 > 0 成功 , < 0 失败int parameters_to_context_result =avcodec_parameters_to_context(avCodecContext, codecParameters);//打开编解码器//   int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, //   返回 0 成功 , 其它失败int open_codec_result = avcodec_open2(avCodecContext, avCodec, 0);//初始化 AVPacket 空数据包AVPacket *avPacket = av_packet_alloc();//读取 AVPacket 数据/*读取数据包 , 并存储到 AVPacket 数据包中参数分析 : 一维指针 与 二维指针 参数分析① 注意 : 第二个参数是 AVPacket * 类型的 , 那么传入 AVPacket *avPacket 变量不能修改 avPacket 指针的指向 , 即该指针指向的结构体不能改变只能修改 avPacket 指向的结构体中的元素的值因此 , 传入的 avPacket 结构体指针必须先进行初始化 , 然后再传入av_read_frame 函数内 , 没有修改 AVPacket *avPacket 的值 , 但是修改了结构体中元素的值② 与此相对应的是 avformat_open_input 方法 , 传入 AVFormatContext ** 二维指针传入的的 AVFormatContext ** 是没有经过初始化的 , 连内存都没有分配在 avformat_open_input 方法中创建并初始化 AVFormatContext * 结构体指针然后将该指针地址赋值给 AVFormatContext **avformat_open_input 函数内修改了 AVFormatContext ** 参数的值返回值 0 说明读取成功 , 小于 0 说明读取失败 , 或者 读取完毕*/int read_frame_result = av_read_frame(formatContext, avPacket);/**  1 . 发送数据包将数据包发送给解码器 , 返回 0 成功 , 其它失败AVERROR(EAGAIN): 说明当前解码器满了 , 不能接受新的数据包了这里先将解码器的数据都处理了, 才能接收新数据其它错误处理 : 直接退出循环*/int result_send_packet = avcodec_send_packet(avCodecContext, avPacket);//2 . 本次循环中 , 将 AVPacket 丢到解码器中解码完毕后 , 就可以释放 AVPacket 内存了av_packet_free(&avPacket);if(result_send_packet != 0){//TODO 失败处理}//3 . 接收并解码数据包 , 存放在 AVFrame 中//用于存放解码后的数据包 , 一个 AVFrame 代表一个图像AVFrame *avFrame = av_frame_alloc();//4 . 解码器中将数据包解码后 , 存放到 AVFrame * 中 , 这里将其取出并解码//  返回 AVERROR(EAGAIN) : 当前状态没有输出 , 需要输入更多数据//  返回 AVERROR_EOF : 解码器中没有数据 , 已经读取到结尾//  返回 AVERROR(EINVAL) : 解码器没有打开int result_receive_frame = avcodec_receive_frame(avCodecContext, avFrame);//失败处理if(result_receive_frame != 0){//TODO 失败处理}//FFMPEG AVFrame 图像格式转换 YUV -> RGBA//1 . 获取转换上下文SwsContext *swsContext = sws_getContext(//源图像的 宽 , 高 , 图像像素格式avCodecContext->width, avCodecContext->height, avCodecContext->pix_fmt,//目标图像 大小不变 , 不进行缩放操作 , 只将像素格式设置成 RGBA 格式的avCodecContext->width, avCodecContext->height, AV_PIX_FMT_RGBA,//使用的转换算法 , FFMPEG 提供了许多转换算法 , 有快速的 , 有高质量的 , 需要自己测试SWS_BILINEAR,//源图像滤镜 , 这里传 NULL 即可0,//目标图像滤镜 , 这里传 NULL 即可0,//额外参数 , 这里传 NULL 即可0);//2 . 初始化图像存储内存//指针数组 , 数组中存放的是指针//存储RGBA数据uint8_t *dst_data[4];//普通的 int 数组//dst_linesize 数组的每个元素都是用来存储对应通道图像数据在内存中一行的字节大小的。int dst_linesize[4];//初始化 dst_data 和 dst_linesize , 为其申请内存 , 注意使用完毕后需要释放内存//有两种申请image空间的方式1.av_image_alloc(dst_data, dst_linesize,avCodecContext->width, avCodecContext->height, AV_PIX_FMT_RGBA,1);2.int av_image_fill_arrays(uint8_t *dst_data[4], int dst_linesize[4],const uint8_t *src,enum AVPixelFormat pix_fmt, int width, int height, int align);//3 . 格式转换sws_scale(//SwsContext *swsContext 转换上下文swsContext,//要转换的数据内容avFrame->data,//数据中每行的字节长度avFrame->linesize,0,avFrame->height,//转换后目标图像数据存放在这里dst_data,//转换后的目标图像行数dst_linesize);

上面的流程都有较为详细的注释,我这边用口头的语句讲述一下ffmpeg基本需要做一些什么操作,以便大家能比较好的理解。

首先ffmpeg需要通过一个方法打开我们的目标视频,其次我们需要通过遍历拿到这个目标视频的视频流id,通过这个视频流我们获取其编码参数,通过编码参数ID获取编码器,拿到编码器之后,便可以获取编码器的上下文了,紧接着我们便可以将编码器的上下文跟编码器参数绑定,这样编码器的相关设置就完成了,记得要打开编码器。

接着我们就可以开始开始循环解码了,首先我们可以创建一个packet,用packet去接收我们的视频数据,通过packet将我们的视频数据送到编解码器中进行解码,声明一个AVFrame,当解码完成后使用avframe去接收解码之后的数据。

但是这个数据的格式YUV的,我们需要讲这些数据显示到ANativeWindow中,故需要进行格式转换,将格式装换成RGBA格式。

以上就是FFMPEG在这个demo中所需要做的。在代码逻辑中也是这个逻辑,只是每个需要做的东西都通过一个方法去执行,所以没有什么好怕的。接下来我将给出一个完整的播放逻辑代码。

FFMPEG Native播放方法

extern "C"
JNIEXPORT void JNICALL
Java_com_example_learnffmpegapplication_FFMediaPlayer_native_1StartToPlayingVideo(JNIEnv *env,jobject thiz,jstring video_path,jobject surface) {// TODO: implement native_StartToPlayingVideo()LogUtils.debug("yjs", "start to play video");const char *mVideoPath = env->GetStringUTFChars(video_path, 0);avformat_network_init();LogUtils.debug("yjs", "avformat_network_init");//这里不能忘记AVFormatContext *mAVFormatContext = avformat_alloc_context();LogUtils.debug("yjs", mVideoPath);AVDictionary *pDictionary = NULL;av_dict_set(&pDictionary, "timeout", "3000000", 0);int open_result = avformat_open_input(&mAVFormatContext, mVideoPath, NULL, nullptr);LogUtils.debug("yjs", "avformat_open_input");if (open_result < 0) {//怀疑没有权限//先排查一下是不是没有权限的原因,把文件放到data文件夹中int err_code;char buf[1024];av_strerror(err_code, buf, 1024);LogUtils.error("yjs", buf);const std::string &string = std::to_string(open_result);const char *str = string.c_str();LogUtils.error("yjs", str);LogUtils.error("yjs", "打开媒体失败");return;} else {LogUtils.debug("yjs", "输入文件成功");}LogUtils.debug("yjs", "avformat_find_stream_info begin");int find_stream_result = avformat_find_stream_info(mAVFormatContext, 0);LogUtils.debug("yjs", "avformat_find_stream_info end");if (find_stream_result < 0) {LogUtils.error("yjs", "查找媒体流失败");}//查找视频流int video_stream_index = -1;for (int i = 0; i < mAVFormatContext->nb_streams; ++i) {if (mAVFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {video_stream_index = i;break;}}AVStream *pAvStream = mAVFormatContext->streams[video_stream_index];AVRational timeBase = pAvStream->time_base;// 计算帧率double frameRate = av_q2d(timeBase);//获取音视频流的编码参数LogUtils.debug("yjs", "获取音视频流的编码参数");AVCodecParameters *codecParameters = pAvStream->codecpar;//查找编解码器LogUtils.debug("yjs", "查找编解码器");AVCodec *avCodec = avcodec_find_decoder(codecParameters->codec_id);//获取编解码器上下文LogUtils.debug("yjs", "获取编解码器上下文");AVCodecContext *avCodecContext = avcodec_alloc_context3(avCodec);//上下文绑定参数LogUtils.debug("yjs", "上下文绑定参数");int parameters_to_context_result =avcodec_parameters_to_context(avCodecContext, codecParameters);if (parameters_to_context_result < 0) {LogUtils.error("yjs", "绑定参数至编解码器上下文有误");}//打开编解码器int open_codec_result = avcodec_open2(avCodecContext, avCodec, 0);//创建图像转换上下文LogUtils.debug("yjs", "创建图像转换上下文");SwsContext *pSwsContext = sws_getContext(avCodecContext->width, avCodecContext->height,avCodecContext->pix_fmt,avCodecContext->width, avCodecContext->height,AV_PIX_FMT_RGBA, SWS_BILINEAR, 0,0, 0);// 获取 ANativeWindow 对象LogUtils.debug("yjs", "获取 ANativeWindow 对象");if (aNativeWindow) {ANativeWindow_release(aNativeWindow);}aNativeWindow = ANativeWindow_fromSurface(env, surface);ANativeWindow_Buffer mNativeWindowBuffer;// 设置渲染格式和大小LogUtils.debug("yjs", "设置渲染格式和大小");ANativeWindow_setBuffersGeometry(aNativeWindow, avCodecContext->width, avCodecContext->height,WINDOW_FORMAT_RGBA_8888);//从视频流读取数据包到avpacketAVPacket *avPacketVideo = av_packet_alloc();while (av_read_frame(mAVFormatContext, avPacketVideo) >= 0) {// 将要解码的数据包送入解码器LogUtils.debug("yjs","将要解码的数据包送入解码器");avcodec_send_packet(avCodecContext, avPacketVideo);AVFrame *avFrameVideo = av_frame_alloc();//从解码器内部缓存中提取解码后的音视频帧int ret = avcodec_receive_frame(avCodecContext, avFrameVideo);if (ret == AVERROR(EAGAIN)) {continue;} else if (ret < 0) {LogUtils.debug("yjs","读取结束");break;}//获取RGBA的VideoFrameAVFrame *m_RGBAFrame = av_frame_alloc();//计算 Buffer 的大小int bufferSize = av_image_get_buffer_size(AV_PIX_FMT_RGBA, avCodecContext->width,avCodecContext->height, 1);uint8_t *m_FrameBuffer = (uint8_t *) av_malloc(bufferSize * sizeof(uint8_t));//填充m_RGBAFrameav_image_fill_arrays(m_RGBAFrame->data, m_RGBAFrame->linesize, m_FrameBuffer,AV_PIX_FMT_RGBA,avCodecContext->width, avCodecContext->height, 1);sws_scale(pSwsContext, avFrameVideo->data, avFrameVideo->linesize, 0, avFrameVideo->height,m_RGBAFrame->data, m_RGBAFrame->linesize);//我们拿到了 RGBA 格式的图像,可以利用 ANativeWindow 进行渲染了。//设置渲染区域和输入格式ANativeWindow_setBuffersGeometry(aNativeWindow, avCodecContext->width,avCodecContext->height, WINDOW_FORMAT_RGBA_8888);//3. 渲染ANativeWindow_Buffer m_NativeWindowBuffer;//锁定当前 Window ,获取屏幕缓冲区 Buffer 的指针ANativeWindow_lock(aNativeWindow, &m_NativeWindowBuffer, nullptr);uint8_t *dstBuffer = static_cast<uint8_t *>(m_NativeWindowBuffer.bits);int srcLineSize = m_RGBAFrame->linesize[0];//输入图的步长(一行像素有多少字节)int dstLineSize = m_NativeWindowBuffer.stride * 4;//RGBA 缓冲区步长for (int i = 0; i < avCodecContext->height; ++i) {//一行一行地拷贝图像数据memcpy(dstBuffer + i * dstLineSize, m_FrameBuffer + i * srcLineSize, srcLineSize);}//解锁当前 Window ,渲染缓冲区数据LogUtils.debug("yjs","解锁当前 Window ,渲染缓冲区数据");ANativeWindow_unlockAndPost(aNativeWindow);av_frame_free(&avFrameVideo);av_frame_free(&m_RGBAFrame);delete(m_FrameBuffer);}LogUtils.debug("yjs","绘制完成");av_packet_unref(avPacketVideo);ANativeWindow_release(aNativeWindow);sws_freeContext(pSwsContext);avcodec_free_context(&avCodecContext);avformat_close_input(&mAVFormatContext);env->ReleaseStringUTFChars(video_path, mVideoPath);
}

整个代码逻辑需要注意的就是在格式转换完之后,我们应该如何将这个转换后的数据放入到ANativeWindow的缓冲区中。

//获取ANaitveWindow对象
aNativeWindow = ANativeWindow_fromSurface(env, surface);
//设置渲染区域和输入格式
ANativeWindow_setBuffersGeometry(aNativeWindow, avCodecContext->width, avCodecContext->height,WINDOW_FORMAT_RGBA_8888);//锁定ANativeWindow渲染缓冲区
ANativeWindow_lock(aNativeWindow, &m_NativeWindowBuffer, nullptr);//获取m_NativeWindowBuffer的bits属性,这个属性就是存储数据的地方,将其转换成m_NativeWindowBuffer指针
uint8_t *dstBuffer = static_cast<uint8_t *>(m_NativeWindowBuffer.bits);
int srcLineSize = m_RGBAFrame->linesize[0];//输入图的步长(一行像素有多少字节)
//计算渲染缓冲区每一行的字节大小,这里乘以 4 是因为通常会使用 RGBA 格式,每个像素占据 4 个字节。
int dstLineSize = m_NativeWindowBuffer.stride * 4;//RGBA 缓冲区步长for (int i = 0; i < avCodecContext->height; ++i) {//一行一行地拷贝图像数据memcpy(dstBuffer + i * dstLineSize, m_FrameBuffer + i * srcLineSize, srcLineSize);
}//解锁当前 Window ,渲染缓冲区数据
ANativeWindow_unlockAndPost(aNativeWindow);

自此,demo便可以实现简单的视频播放功能了。

总结 

其实这个demo的实现真的很简单,只需要熟悉FFMPEG的一个基本流程便可轻松实现。后续我会将这个demo的源码上传,大家有需要的可以进行下载,或者私信我直接给你们发。

相关文章:

FFMPEG+ANativeWinodow渲染播放视频

前言 学习音视频开发&#xff0c;入门基本都得学FFMPEG&#xff0c;按照目前互联网上流传的学习路线&#xff0c;FFMPEGANativeWinodow渲染播放视频属于是第一关卡的Boss&#xff0c;简单但是关键。这几天写了个简单的demo&#xff0c;可以比较稳定进行渲染播放&#xff0c;便…...

使用AXI MIG/Proc Sys Reset

使用AXI MIG/Proc Sys Reset 重要&#xff01;仅当您的设计中包含AXI MIG时&#xff0c;才执行以下步骤。 AXI-MIG的连接接口 1.选择在/mig_7series_0/S_AXI上运行连接自动化。 2.选择/micblaze_0&#xff08;缓存&#xff09;或/micblaze _0&#xff08;Periph&#xff09;选项…...

Android基础-Kotlin语言的作用及优缺点

一、Kotlin语言的作用 Kotlin是一种由JetBrains公司开发的现代化静态类型编程语言&#xff0c;自其诞生以来&#xff0c;便在多个领域展现出了强大的应用潜力。其主要作用可以概括为以下几点&#xff1a; Android应用开发&#xff1a;Kotlin作为Android开发的官方推荐语言&am…...

手机投屏技巧:手机怎么投屏到电脑显示屏上?精选6招解决!

手机怎么投屏到电脑显示屏上&#xff1f;出于一些不同的原因&#xff0c;大多数人都希望能将手机投屏到电脑上。其中一个常见的原因是&#xff0c;大家经常会希望在笔记本电脑上共享图片&#xff0c;而无需上传或者登录微信进行文件传输。以及希望不依靠投影仪&#xff0c;就能…...

内存函数<C语言>

前言 前面两篇文章介绍了字符串函数&#xff0c;不过它们都只能用来处理字符串&#xff0c;C语言中也内置了一些内存函数来对不同类型的数据进行处理&#xff0c;本文将介绍&#xff1a;memcpy()使用以及模拟实现&#xff0c;memmove()使用以及模拟实现&#xff0c;memset()使用…...

华为校招机试 - LRU模拟(20240515)

题目描述 LRU(Least Recently Used)缓存算法是一种常用于管理缓存的策略,其目标是保留最近使用过的数据,而淘汰最久未被使用的数据。 实现简单的LRU缓存算法,支持查询、插入、删除操作。 最久未被使用定义:查询、插入和删除操作均为一次访问操作,每个元素均有一个最后…...

AI预测福彩3D采取888=3策略+和值012路一缩定乾坤测试5月29日预测第5弹

今天继续基于8883的大底&#xff0c;使用尽可能少的条件进行缩号&#xff0c;同时&#xff0c;同样准备两套方案&#xff0c;一套是我自己的条件进行缩号&#xff0c;另外一套是8883的大底结合2码不定位奖号预测二次缩水来杀号。好了&#xff0c;直接上结果吧~ 首先&…...

03_前端三大件CSS

文章目录 CSS用于页面元素美化1.CSS引入1.1style方式1.2写入head中&#xff0c;通过写style然后进行标签选择器加载样式1.3外部样式表 2.CSS样式选择器2.1 元素选择器2.2 id选择器2.3 class选择器 3.CSS布局相关3.1 CSS浮动背景&#xff1a;先设计一些盒子因此&#xff0c;引出…...

十种常用数据分析模型

1-线性回归&#xff08;Linear Regression&#xff09; 场景&#xff1a;预测商品销售额 优点&#xff1a;简单易用&#xff0c;结果易于解释缺点&#xff1a;假设线性关系&#xff0c;容易受到异常值影响概念&#xff1a;建立自变量和因变量之间线性关系的模型。公式&#x…...

salesforce 公式字段 判断一个字段是否在某个多选列表中

在 Salesforce 中&#xff0c;你可以使用公式字段来判断一个字段的值是否在一个多选列表中。这通常涉及使用包含特定值的函数和一些字符串操作。以下是一个常见的方法&#xff1a; 假设你有一个多选列表字段 Multi_Select_Field__c&#xff0c;你想检查这个字段是否包含某个值…...

C++STL容器系列(三)list的详细用法和底层实现

目录 一&#xff1a;介绍二&#xff1a;list的创建和方法创建list方法 三&#xff1a;list的具体用法3.1 push_back、pop_back、push_front、pop_front3.2 insert() 和 erase()3.3 splice 函数 四&#xff1a;list容器底层实现4.1 list 容器节点结构5.2 list容器迭代器的底层实…...

IEEE Latex模版踩雷避坑指南

参考文献 原Latex模版 \begin{thebibliography}{1} \bibliographystyle{IEEEtran}\bibitem{ref1} {\it{Mathematics Into Type}}. American Mathematical Society. [Online]. Available: https://www.ams.org/arc/styleguide/mit-2.pdf\bibitem{ref2} T. W. Chaundy, P. R. Ba…...

【C++】类与对象——多态详解

目录 一、多态的定义 二、重载、覆盖(重写)、隐藏(重定义)的对比 三、析构函数重写 四、C11 override 和 final 1. final 2. override 五、抽象类 六、多态的原理 一、多态的定义 多态是在不同继承关系的类对象&#xff0c;去调用同一函数&#xff0c;产生了不同的行为…...

WordPress建网站公司 建易WordPress建站

建易WordPress建网站公司是一家专业从事WordPress网站建设、网站维护、网站托管、运营推广和搜索引擎优化(SEO)等服务的公司。建易WordPress建网站公司提供多种服务&#xff0c;包括模板建站和定制网站&#xff0c;并且明码标价&#xff0c;价格透明&#xff0c;竭诚为全国各地…...

MySQL正则替换整个单词

\b 是正则表达式规定的一个特殊代码&#xff08;好吧&#xff0c;某些人叫它元字符&#xff0c;metacharacter&#xff09;&#xff0c;代表着单词的开头或结尾&#xff0c;也就是单词的分界处。虽然通常英文的单词是由空格&#xff0c;标点符号或者换行来分隔的&#xff0c;但…...

Java设计模式:享元模式实现高效对象共享与内存优化(十一)

码到三十五 &#xff1a; 个人主页 目录 一、引言二、享元设计模式的概念1. 对象状态的划分2. 共享机制 三、享元设计模式的组成四、享元设计模式的工作原理五、享元模式的使用六、享元设计模式的优点和适用场景结语 [参见]&#xff1a; Java设计模式&#xff1a;核心概述&…...

景源畅信电商:抖音开店步骤是什么?

随着社交媒体的兴起&#xff0c;抖音已经成为一个不可忽视的电商平台。许多人都希望通过抖音开店来实现自己的创业梦想。那么&#xff0c;抖音开店的具体步骤是什么呢?接下来&#xff0c;我们将详细阐述这一问题。 一、明确回答问题抖音开店的步骤主要包括&#xff1a;注册账号…...

Notepad++不显示CRLF的方法

View -> Show Symbol -> 去掉勾选 Show All Characters...

前端开发工程师——AngularJS

一.表达式和语句 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"widthdevice-w…...

【AI算法岗面试八股面经【超全整理】——概率论】

AI算法岗面试八股面经【超全整理】 概率论信息论机器学习CVNLP 目录 1、古典概型、几何概型2、条件概率、全概率公式、贝叶斯公式3、先验概率、后验概率4、离散型随机变量的常见分布5、连续型随机变量的常见分别6、数学期望、方差7、协方差、相关系数8、独立、互斥、不相关9.大…...

Zustand 状态管理库:极简而强大的解决方案

Zustand 是一个轻量级、快速和可扩展的状态管理库&#xff0c;特别适合 React 应用。它以简洁的 API 和高效的性能解决了 Redux 等状态管理方案中的繁琐问题。 核心优势对比 基本使用指南 1. 创建 Store // store.js import create from zustandconst useStore create((set)…...

可靠性+灵活性:电力载波技术在楼宇自控中的核心价值

可靠性灵活性&#xff1a;电力载波技术在楼宇自控中的核心价值 在智能楼宇的自动化控制中&#xff0c;电力载波技术&#xff08;PLC&#xff09;凭借其独特的优势&#xff0c;正成为构建高效、稳定、灵活系统的核心解决方案。它利用现有电力线路传输数据&#xff0c;无需额外布…...

理解 MCP 工作流:使用 Ollama 和 LangChain 构建本地 MCP 客户端

&#x1f31f; 什么是 MCP&#xff1f; 模型控制协议 (MCP) 是一种创新的协议&#xff0c;旨在无缝连接 AI 模型与应用程序。 MCP 是一个开源协议&#xff0c;它标准化了我们的 LLM 应用程序连接所需工具和数据源并与之协作的方式。 可以把它想象成你的 AI 模型 和想要使用它…...

基于Uniapp开发HarmonyOS 5.0旅游应用技术实践

一、技术选型背景 1.跨平台优势 Uniapp采用Vue.js框架&#xff0c;支持"一次开发&#xff0c;多端部署"&#xff0c;可同步生成HarmonyOS、iOS、Android等多平台应用。 2.鸿蒙特性融合 HarmonyOS 5.0的分布式能力与原子化服务&#xff0c;为旅游应用带来&#xf…...

在四层代理中还原真实客户端ngx_stream_realip_module

一、模块原理与价值 PROXY Protocol 回溯 第三方负载均衡&#xff08;如 HAProxy、AWS NLB、阿里 SLB&#xff09;发起上游连接时&#xff0c;将真实客户端 IP/Port 写入 PROXY Protocol v1/v2 头。Stream 层接收到头部后&#xff0c;ngx_stream_realip_module 从中提取原始信息…...

Cloudflare 从 Nginx 到 Pingora:性能、效率与安全的全面升级

在互联网的快速发展中&#xff0c;高性能、高效率和高安全性的网络服务成为了各大互联网基础设施提供商的核心追求。Cloudflare 作为全球领先的互联网安全和基础设施公司&#xff0c;近期做出了一个重大技术决策&#xff1a;弃用长期使用的 Nginx&#xff0c;转而采用其内部开发…...

【JavaWeb】Docker项目部署

引言 之前学习了Linux操作系统的常见命令&#xff0c;在Linux上安装软件&#xff0c;以及如何在Linux上部署一个单体项目&#xff0c;大多数同学都会有相同的感受&#xff0c;那就是麻烦。 核心体现在三点&#xff1a; 命令太多了&#xff0c;记不住 软件安装包名字复杂&…...

AGain DB和倍数增益的关系

我在设置一款索尼CMOS芯片时&#xff0c;Again增益0db变化为6DB&#xff0c;画面的变化只有2倍DN的增益&#xff0c;比如10变为20。 这与dB和线性增益的关系以及传感器处理流程有关。以下是具体原因分析&#xff1a; 1. dB与线性增益的换算关系 6dB对应的理论线性增益应为&…...

【Go语言基础【12】】指针:声明、取地址、解引用

文章目录 零、概述&#xff1a;指针 vs. 引用&#xff08;类比其他语言&#xff09;一、指针基础概念二、指针声明与初始化三、指针操作符1. &&#xff1a;取地址&#xff08;拿到内存地址&#xff09;2. *&#xff1a;解引用&#xff08;拿到值&#xff09; 四、空指针&am…...

并发编程 - go版

1.并发编程基础概念 进程和线程 A. 进程是程序在操作系统中的一次执行过程&#xff0c;系统进行资源分配和调度的一个独立单位。B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。C.一个进程可以创建和撤销多个线程;同一个进程中…...