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-libs
        avformat
        avcodec
        avfilter
        swresample
        swscale
        avutil
        )

#定义需要链接的系统本地库,例如 Android、EGL、GLESv3、OpenSLES、log、m 和 z 等。
set(native-libs
        android
        EGL
        GLESv3
        OpenSLES
        log
        m
        z
        )

指定 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">
    <Button
        android:id="@+id/selectmp4btn"
        android:text="选择MP4文件"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
    <SurfaceView
        android: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;

    @Override
    protected 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);
    }

    @Override
    public 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();
            }
        }
    }

    @Override
    public void surfaceCreated(@NonNull SurfaceHolder holder) {
        Log.d("yjs", "onSurfaceCreated");
        mFFMediaPlayer = new FFMediaPlayer();

    }

    @Override
    public 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() {
            @Override
            public void run() {
                mFFMediaPlayer.startPlayingVideo(mVideoPath, ANativeWindowSurface);
            }
        }).start();
    }

    @Override
    public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
        Log.d("yjs", "onSurfaceDestroyed");
    }

    @Override
    protected 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);

            }
        }

    }

    @Override
    public 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发生改变的时候通过播放工具类进行视频播放。

@Override
    public void surfaceCreated(@NonNull SurfaceHolder holder) {
        Log.d("yjs", "onSurfaceCreated");
        mFFMediaPlayer = new FFMediaPlayer();

    }

    @Override
    public 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() {
            @Override
            public void run() {
                mFFMediaPlayer.startPlayingVideo(mVideoPath, ANativeWindowSurface);
            }
        }).start();
    }

    @Override
    public 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);

    //从视频流读取数据包到avpacket
    AVPacket *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的VideoFrame
        AVFrame *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_RGBAFrame
        av_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的源码上传,大家有需要的可以进行下载,或者私信我直接给你们发。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/662404.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

软件系统测试的类型和方法介绍

测试是软件开发过程中至关重要的一环&#xff0c;负责验证和确认软件系统是否符合预期的需求&#xff0c;并帮助开发团队消除潜在的缺陷。系统测试作为软件测试中不可缺少的过程&#xff0c;是根据预先制定的测试计划和测试用例&#xff0c;以检查软件系统功能、性能、安全性和…

LED显示屏模组七大参数

LED模组是LED显示屏的核心组件&#xff0c;它包含LED线路板和外壳&#xff0c;将LED灯珠按照特定规则排列并封装&#xff0c;通常还会进行防水处理。随着LED显示屏行业的发展及其广泛应用&#xff0c;LED模组的功能和作用变得愈加重要。那么&#xff0c;LED模组的七大参数是什么…

开抖店必须要办理营业执照吗?不用营业执照开店的个人店能用吗?

大家好&#xff0c;我是电商花花。 可能大家都发现了&#xff0c;抖音小店个人店不用营业执照&#xff0c;只凭借身份证就能开店。 但是这个个人店花花并不建议大家去开&#xff0c;虽然说用用身份证也能开店&#xff0c;有效的帮我们减少了开店的成本&#xff0c;但是个人店…

【RLHF个人笔记】RLHF:Reinforcement Learning from Human Feedback具体过程

【RLHF个人笔记】RLHF:Reinforcement Learning from Human Feedback具体过程 RLHF训练的三个步骤步骤1&#xff1a;收集数据与有监督训练策略步骤2&#xff1a;收集数据训练奖励模型步骤3&#xff1a;结合奖励模型利用强化学习算法如PPO算法来优化策略 参考内容 RLHF训练的三个…

jeecgboot 同一账号只允许一个人登录

1.需求分析 jeecgboot 框架要实现同一个账号只允许一个人登录&#xff0c;就跟游戏账号类似&#xff0c;“我登录了就把你踢下去&#xff0c;你登录了就把我踢下去”&#xff1b;jwt 原理是生成 token 后一段时间内登录都有效&#xff0c;jeecgboot 中 jwt 和 redis 联合使用后…

易备数据备份软件:快速恢复 VMware ESXi 虚拟机

易备数据备份软件为 VMware ESXi 虚拟机提供完整的保护和备份功能。软件同时支持从 ESXi 或 vCenter 虚拟机的增量和差异备份中进行自动恢复。支持精细化的恢复&#xff0c;可将虚拟机恢复到某个特定的日期。 通过易备数据备份软件&#xff0c;可以实现虚拟机的异机恢复&#…

深入理解JVM:内存结构、垃圾收集与性能调优

目录 JDK、JRE、JVM关系? 启动程序如何查看加载了哪些类&#xff0c;以及加载顺序? class字节码文件10个主要组成部分? JVM结构 画一下JVM内存结构图 程序计数器 Java虚拟机栈 本地方法栈 Java堆 方法区 运行时常量池? 什么时候抛出StackOverflowError? 例如&…

元宇宙对于品牌营销有哪些影响?品牌如何加入?

元宇宙对于品牌营销带来了许多新的营销方式和策略&#xff0c;这些方式在传统营销中是无法实现的。以下是元宇宙对于品牌营销的主要营销方式&#xff1a; 1、虚拟展示&#xff1a; 利用元宇宙技术&#xff0c;品牌可以将产品或服务在虚拟世界中进行展示&#xff0c;用户可以通…

Leetcode - 131双周赛

一&#xff0c;3158. 求出出现两次数字的 XOR 值 本题是一道纯模拟题&#xff0c;直接暴力。 代码如下&#xff1a; class Solution {public int duplicateNumbersXOR(int[] nums) {int ans 0;long t 0;for(int x : nums){if(((t>>x)&1) 1){ans ^ x;}else{t | (…

深度神经网络——什么是迁移学习?

1.概述 在练习机器学习时&#xff0c;训练模型可能需要很长时间。从头开始创建模型架构、训练模型&#xff0c;然后调整模型需要大量的时间和精力。训练机器学习模型的一种更有效的方法是使用已经定义的架构&#xff0c;可能具有已经计算出的权重。这是背后的主要思想 迁移学习…

适合多种苛刻环境的惯性测量单元M-G370PDS

全球IMU市场d在汽车和机器人技术进步和不断增长的应用需求&#xff0c;保持着高速增长的趋势&#xff0c;其中航空航天、国防和汽车等行业对高精度、稳定和紧凑的IMU需求尤为强烈&#xff0c;这些行业对精度和可靠性的高要求直接影响了相关技术的发展方向。 爱普生惯性测量单…

现场辩论赛活动策划方案

活动目的&#xff1a; 技能竞赛中的辩论环节既可以考核员工的知识点&#xff0c;同时也可以考核员工业务办事能力&#xff0c;表达能力&#xff0c;是一种比较全面且较有深度的竞赛方式。 辩论赛细则&#xff1a; 1、时间提示 : 自由辩论阶段&#xff0c;每方使用时间剩…

如何将md文件精确的转换成docx文件

如何将md文件转换成docx&#xff1f; 文章目录 如何将md文件转换成docx&#xff1f;一、如何将MD文件比较完美的转换成word呢&#xff1f;二、方法3 步骤1、下载一个可用的MarkDown编辑器2、下载Pandoc安装 三、来进行转化了 一、如何将MD文件比较完美的转换成word呢&#xff1…

基于51单片机智能蓝牙台灯

基于51单片机智能蓝牙台灯 &#xff08;仿真&#xff0b;程序&#xff0b;原理图PCB&#xff09; 功能介绍 具体功能&#xff1a; 1.分为手动/自动两种模式&#xff0c;自动模式下对应LED指示灯亮&#xff1b; 2.手动模式下&#xff0c;可用按键调节亮度&#xff1b; 3.自动…

AI 画图真刺激,手把手教你如何用 ComfyUI 来画出刺激的图

目前 AI 绘画领域的产品非常多&#xff0c;比如 Midjourney、Dalle3、Stability AI 等等&#xff0c;这些产品大体上可以分为两类&#xff1a; 模型与产品深度融合&#xff1a;比如 Midjourney、Dalle3 等等。模型与产品分离&#xff1a;比如 SD Web UI、ComfyUI 等等。 对于…

使用jdk自带jhat工具排查OOM问题

使用jdk自带jhat工具排查OOM问题 OOM java.lang.OutOfMemoryError: Java heap space排查步骤 编写一个测试类 public class TestJVM {Testpublic void test1() throws InstantiationException, IllegalAccessException {List<A> list new ArrayList<>();for (i…

Java开发-面试题-0001-String、StringBuilder、StringBuffer的区别

Java开发-面试题-0001-String、StringBuilder、StringBuffer的区别 更多内容欢迎关注我&#xff08;持续更新中&#xff0c;欢迎Star✨&#xff09; Github&#xff1a;CodeZeng1998/Java-Developer-Work-Note 技术公众号&#xff1a;CodeZeng1998&#xff08;纯纯技术文&am…

OLED显示一张图片

1.思路: void Oled_Show_Image(unsigned char *image) // { unsigned char i; //-128 ~ 127位 unsigned int j; //j要重新定义&#xff0c;因为要到达图片的最后一位 //行 i没有问题&#xff0c;j有问题 i为1时&am…

光速进化!易天万兆光模块全面升级

易天光通信宣布10G SFP/25G SFP28系列光模块产品进行了全新升级&#xff0c;旨在为客户提供更优质、更高效、更可靠的光通信解决方案。这次升级不仅是技术的突破&#xff0c;更是对未来光通信发展趋势的深刻洞察和精准把握。 一、技术革新&#xff0c;性能卓越 本次全系列产品…

强化学习——学习笔记3

一、强化学习都有哪些分类&#xff1f; 1、基于模型与不基于模型 根据是否具有环境模型&#xff0c;强化学习算法分为两种&#xff1a;基于模型与不基于模型 基于模型的强化学习(Model-based RL)&#xff1a;可以简单的使用动态规划求解&#xff0c;任务可定义为预测和控制&am…