Android 使用FFmpeg解析RTSP流,ANativeWindow渲染 使用SurfaceView播放流程详解

文章目录

    • ANativeWindow 介绍
      • `ANativeWindow` 的主要功能和特点
      • `ANativeWindow` 的常用函数
      • 工作流程原理图
      • 通过ANativeWindow渲染RGB纯色示例
    • 播放RTSP流工作流程图
    • 关键步骤解析
      • 自定义SurfaceView组件
      • native 层解码渲染
    • 效果展示
    • 注意事项

这篇文章涉及到jni层,以及Ffmpeg编解码原理,不了解相关观念的,可以先看相关技术介绍

传送门:

JNI入门_Trump. yang的博客-CSDN博客

音视频开发_Trump. yang的博客-CSDN博客

ANativeWindow 介绍

ANativeWindow 是 Android NDK 中的一个类,用于在 Native 层处理和渲染窗口。它提供了一组函数,用于在本地代码中直接操作 Android 视图系统,以便更高效地进行图像和视频渲染。ANativeWindow 通常与 SurfaceSurfaceViewSurfaceTexture 等一起使用。

ANativeWindow 的主要功能和特点

  1. 窗口抽象层
    ANativeWindow 提供了一个窗口抽象层,使得本地代码能够直接操作窗口的像素数据。它可以从 Java 层的 Surface 对象获取,并用于渲染图像或视频。

  2. 锁定和解锁缓冲区
    通过 ANativeWindow_lockANativeWindow_unlockAndPost 函数,开发者可以锁定窗口的缓冲区进行像素操作,完成后解锁并提交缓冲区进行显示。

  3. 设置缓冲区属性
    可以使用 ANativeWindow_setBuffersGeometry 来设置缓冲区的大小和像素格式,以适应不同的渲染需求。

  4. 高效渲染
    直接在 Native 层操作窗口的缓冲区,可以减少数据传输和转换的开销,提高渲染性能。

ANativeWindow 的常用函数

  1. 获取 ANativeWindow 对象
    从 Java 层的 Surface 对象获取 ANativeWindow 对象。

    ANativeWindow* ANativeWindow_fromSurface(JNIEnv* env, jobject surface);
    
  2. 设置缓冲区属性
    设置缓冲区的大小和像素格式。

    int ANativeWindow_setBuffersGeometry(ANativeWindow* window, int width, int height, int format);
    
  3. 锁定缓冲区
    锁定窗口的缓冲区以进行像素操作。

    int ANativeWindow_lock(ANativeWindow* window, ANativeWindow_Buffer* outBuffer, ARect* inOutDirtyBounds);
    
  4. 解锁缓冲区并提交
    解锁并提交缓冲区,显示内容。

    int ANativeWindow_unlockAndPost(ANativeWindow* window);
    
  5. 释放 ANativeWindow 对象
    释放 ANativeWindow 对象以释放资源。

    void ANativeWindow_release(ANativeWindow* window);
    

工作流程原理图

在这里插入图片描述

通过ANativeWindow渲染RGB纯色示例

ANativeWindow通常和SurfaceView一块使用,首先自定义一个SurfaceView组件

public class RtspPlayerView extends SurfaceView implements SurfaceHolder.Callback {private SurfaceHolder holder;public RtspPlayerView(Context context, AttributeSet attrs) {super(context, attrs);init();}public RtspPlayerView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init();}private void init() {holder = getHolder();holder.addCallback(this);holder.setFormat(PixelFormat.RGBA_8888);  //设置像素格式}@Overridepublic void surfaceCreated(SurfaceHolder holder) {Log.i("RtspPlayerView", "Surface 创建成功");//传入 RGB数据给Native层String bufferedImage = rgb2Hex(255, 255, 0);String substring = String.valueOf(bufferedImage).substring(3);int color = Integer.parseInt(substring,16);drawToSurface(holder.getSurface(),color);}public void play(String uri) {this.url = uri;}@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {Log.i("RtspPlayerView", "Surface 大小或格式变化");}@Overridepublic void surfaceDestroyed(SurfaceHolder holder) {Log.i("RtspPlayerView", "Surface 销毁");}public static String  rgb2Hex(int r,int g,int b){return String.format("0xFF%02X%02X%02X", r,g,b);}public static native void drawToSurface(Surface surface, int color);}

在自定义组件中 声明一个jni接口,以便于和native层的ANativeWindow交互,注意的是需要向native传递Surface对象的引用和RGB值

    public static native void drawToSurface(Surface surface, int color);

在native层实现C++代码, 较为简单

extern "C"
JNIEXPORT void JNICALL
Java_com_marxist_firstjni_player_RtspPlayerView_drawToSurface(JNIEnv *env, jclass clazz,jobject surface, jint color) {ANativeWindow_Buffer nwBuffer;LOGI("ANativeWindow_fromSurface ");ANativeWindow *mANativeWindow = ANativeWindow_fromSurface(env, surface);if (mANativeWindow == NULL) {LOGE("ANativeWindow_fromSurface error");return;}LOGI("ANativeWindow_lock ");if (0 != ANativeWindow_lock(mANativeWindow, &nwBuffer, 0)) {LOGE("ANativeWindow_lock error");return;}LOGI("ANativeWindow_lock nwBuffer->format ");if (nwBuffer.format == WINDOW_FORMAT_RGBA_8888) {LOGI("nwBuffer->format == WINDOW_FORMAT_RGBA_8888 ");for (int i = 0; i < nwBuffer.height * nwBuffer.width; i++) {*((int*)nwBuffer.bits + i) = color;}}LOGI("ANativeWindow_unlockAndPost ");if (0 != ANativeWindow_unlockAndPost(mANativeWindow)) {LOGE("ANativeWindow_unlockAndPost error");return;}ANativeWindow_release(mANativeWindow);LOGI("ANativeWindow_release ");
}

运行效果:中间那块就是Surfaceview 展示了RGB颜色

在这里插入图片描述

播放RTSP流工作流程图

在这里插入图片描述

关键步骤解析

自定义SurfaceView组件

与加载纯色RGB基本一致,只有jni接口不同

package com.marxist.firstjni.player;import android.content.Context;
import android.graphics.PixelFormat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;public class RtspPlayerView extends SurfaceView implements SurfaceHolder.Callback {private SurfaceHolder holder;private String url;public RtspPlayerView(Context context, AttributeSet attrs) {super(context, attrs);init();}public RtspPlayerView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init();}private void init() {holder = getHolder();holder.addCallback(this);holder.setFormat(PixelFormat.RGBA_8888);Log.i("RtspPlayerView", "我被初始化了");}@Overridepublic void surfaceCreated(SurfaceHolder holder) {Log.i("RtspPlayerView", "Surface 创建成功");
//        decodeVideo("rtsp://192.168.31.165:8554/test",getHolder().getSurface());//传入 RGB数据给Native层
//        String bufferedImage = rgb2Hex(255, 255, 0);
//        String substring = String.valueOf(bufferedImage).substring(3);
//        int color = Integer.parseInt(substring,16);
//
//        drawToSurface(holder.getSurface(),color);//if (url != null && !url.isEmpty()) {new Thread(new Runnable() {@Overridepublic void run() {decodeVideo(url, holder.getSurface());}}).start();}}public void play(String uri) {this.url = uri;}@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {Log.i("RtspPlayerView", "Surface 大小或格式变化");}@Overridepublic void surfaceDestroyed(SurfaceHolder holder) {Log.i("RtspPlayerView", "Surface 销毁");}private native void decodeVideo(String rtspUrl, Surface surface);public static String  rgb2Hex(int r,int g,int b){return String.format("0xFF%02X%02X%02X", r,g,b);}public static native void drawToSurface(Surface surface, int color);}

native 层解码渲染

extern "C"
JNIEXPORT void JNICALL
Java_com_marxist_firstjni_player_RtspPlayerView_decodeVideo(JNIEnv *env, jobject thiz,jstring rtspUrl, jobject surface) {const char *uri = env->GetStringUTFChars(rtspUrl, 0);// 解码视频,解码音频类似,解码的流程类似,把之前的代码拷过来avformat_network_init();AVFormatContext *pFormatContext = NULL;int formatOpenInputRes = 0;int formatFindStreamInfoRes = 0;int audioStramIndex = -1;AVCodecParameters *pCodecParameters;AVCodec *pCodec = NULL;AVCodecContext *pCodecContext = NULL;int codecParametersToContextRes = -1;int codecOpenRes = -1;int index = 0;AVPacket *pPacket = NULL;AVFrame *pFrame = NULL;formatOpenInputRes = avformat_open_input(&pFormatContext, uri, NULL, NULL);if(formatOpenInputRes<0){LOGE("open url error : %s", av_err2str(formatOpenInputRes));return;}formatFindStreamInfoRes = avformat_find_stream_info(pFormatContext, NULL);// 查找视频流的 indexaudioStramIndex = av_find_best_stream(pFormatContext, AVMediaType::AVMEDIA_TYPE_VIDEO, -1, -1,NULL, 0);// 查找解码pCodecParameters = pFormatContext->streams[audioStramIndex]->codecpar;pCodec = avcodec_find_decoder(pCodecParameters->codec_id);// 打开解码器pCodecContext = avcodec_alloc_context3(pCodec);codecParametersToContextRes = avcodec_parameters_to_context(pCodecContext, pCodecParameters);codecOpenRes = avcodec_open2(pCodecContext, pCodec, NULL);// 1. 获取窗体ANativeWindow *pNativeWindow = ANativeWindow_fromSurface(env, surface);if(pNativeWindow == NULL){LOGE("获取窗体失败");return ;}// 2. 设置缓存区的数据ANativeWindow_setBuffersGeometry(pNativeWindow, pCodecContext->width, pCodecContext->height,WINDOW_FORMAT_RGBA_8888);// Window 缓冲区的 BufferANativeWindow_Buffer outBuffer;// 3.初始化转换上下文SwsContext *pSwsContext = sws_getContext(pCodecContext->width, pCodecContext->height,pCodecContext->pix_fmt, pCodecContext->width, pCodecContext->height,AV_PIX_FMT_RGBA, SWS_BILINEAR, NULL, NULL, NULL);AVFrame *pRgbaFrame = av_frame_alloc();int frameSize = av_image_get_buffer_size(AV_PIX_FMT_RGBA, pCodecContext->width,pCodecContext->height, 1);uint8_t *frameBuffer = (uint8_t *) malloc(frameSize);av_image_fill_arrays(pRgbaFrame->data, pRgbaFrame->linesize, frameBuffer, AV_PIX_FMT_RGBA,pCodecContext->width, pCodecContext->height, 1);pPacket = av_packet_alloc();pFrame = av_frame_alloc();while (av_read_frame(pFormatContext, pPacket) >= 0) {if (pPacket->stream_index == audioStramIndex) {// Packet 包,压缩的数据,解码成 数据int codecSendPacketRes = avcodec_send_packet(pCodecContext, pPacket);if (codecSendPacketRes == 0) {int codecReceiveFrameRes = avcodec_receive_frame(pCodecContext, pFrame);if (codecReceiveFrameRes == 0) {// AVPacket -> AVFrameindex++;LOGE("解码第 %d 帧", index);// 假设拿到了转换后的 RGBA 的 data 数据,如何渲染,把数据推到缓冲区sws_scale(pSwsContext, (const uint8_t *const *) pFrame->data, pFrame->linesize,0, pCodecContext->height, pRgbaFrame->data, pRgbaFrame->linesize);// 把数据推到缓冲区if (ANativeWindow_lock(pNativeWindow, &outBuffer, NULL) < 0) {// Handle errorLOGE("ANativeWindow_lock is ERROR");}
// Data copymemcpy(outBuffer.bits, frameBuffer, frameSize);if (ANativeWindow_unlockAndPost(pNativeWindow) < 0) {// Handle errorLOGE("ANativeWindow_unlockAndPost is ERROR");}}}}// 解引用av_packet_unref(pPacket);av_frame_unref(pFrame);}// 1. 解引用数据 data , 2. 销毁 pPacket 结构体内存  3. pPacket = NULLav_packet_free(&pPacket);av_frame_free(&pFrame);__av_resources_destroy:if (pCodecContext != NULL) {avcodec_close(pCodecContext);avcodec_free_context(&pCodecContext);pCodecContext = NULL;}if (pFormatContext != NULL) {avformat_close_input(&pFormatContext);avformat_free_context(pFormatContext);pFormatContext = NULL;}avformat_network_deinit();env->ReleaseStringUTFChars(rtspUrl, uri);
}

在解码之前先创建ANativeWindow对象,设置缓冲区,设置像素格式 一般解码出来的都是yuv 因此要转为RGB,设置转换上下文

// 1. 获取窗体
ANativeWindow *pNativeWindow = ANativeWindow_fromSurface(env, surface);
if(pNativeWindow == NULL){LOGE("获取窗体失败");return ;
}
// 2. 设置缓存区的数据
ANativeWindow_setBuffersGeometry(pNativeWindow, pCodecContext->width, pCodecContext->height,WINDOW_FORMAT_RGBA_8888);
// Window 缓冲区的 Buffer
ANativeWindow_Buffer outBuffer;
// 3.初始化转换上下文
SwsContext *pSwsContext = sws_getContext(pCodecContext->width, pCodecContext->height,pCodecContext->pix_fmt, pCodecContext->width, pCodecContext->height,AV_PIX_FMT_RGBA, SWS_BILINEAR, NULL, NULL, NULL);

在解码之后

  // 假设拿到了转换后的 RGBA 的 data 数据,如何渲染,把数据推到缓冲区sws_scale(pSwsContext, (const uint8_t *const *) pFrame->data, pFrame->linesize,0, pCodecContext->height, pRgbaFrame->data, pRgbaFrame->linesize);// 把数据推到缓冲区if (ANativeWindow_lock(pNativeWindow, &outBuffer, NULL) < 0) {// Handle errorLOGE("ANativeWindow_lock is ERROR");}
// Data copymemcpy(outBuffer.bits, frameBuffer, frameSize);if (ANativeWindow_unlockAndPost(pNativeWindow) < 0) {// Handle errorLOGE("ANativeWindow_unlockAndPost is ERROR");}

往缓冲区里传递转化好的RGB数据

锁定缓冲区,提交数据,交给Surface展示

效果展示

在这里插入图片描述

FFmpeg原生操作延迟果然很低,经测试,局域网能到140ms左右,之前调用第三方库,300ms左右

注意事项

  • 如果闪退,发现ANativeWindow对象为空,说明Surface对象还没有创建完毕,一定要等SurfaceView 创建完毕再进行其他操作。

  • 如果发现解码成功,SurfaceView无法显示,缓冲区操作也正常的话,说明SurfaceView显示被堵塞了,一定要放入到子线程中进行展示

  • 上述代码也可以改成本地文件路径进行解码播放,只需要改动url即可,支持网络也支持本地

参考文章:
https://blog.csdn.net/cjzjolly/article/details/140448984
https://www.jianshu.com/p/e6f2fe8c6afd
https://blog.csdn.net/qq_45396088/article/details/124123280

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux-开机自动挂载(文件系统、交换空间)

准备磁盘 添加三块磁盘&#xff08;两块SATA&#xff0c;一块NVMe&#xff09; 查看设备&#xff1a; [rootlocalhost jian]# ll /dev/sd* [rootlocalhost jian]# ll /dev/nvme0n2 扩&#xff1a;查看当前主机上的所有块设备&#xff0c;通过如下指令实现&#xff1a; [root…

基于语音识别的会议记录系统

文章目录 核心功能页面展示使用技术方案功能结构设计数据库表展示 核心功能页面展示 视频展示功能 1.创建会议 在开始会议之前需要管理员先创建一个会议&#xff0c;为了能够快速开始会议&#xff0c;仅需填写会议的名称、会议举办小组、会议背景等简要会议信息即可成功创建。…

c# .net core中间件,生命周期

某些模块和处理程序具有存储在 Web.config 中的配置选项。但是在 ASP.NET Core 中&#xff0c;使用新配置模型取代了 Web.config。 HTTP 模块和处理程序如何工作 官网地址&#xff1a; 将 HTTP 处理程序和模块迁移到 ASP.NET Core 中间件 | Microsoft Learn 处理程序是&#xf…

【iOS】——内存分区

内存管理 程序运行的过程中往往会涉及到创建对象、定义变量、调用函数或方法&#xff0c;而这些行为都会增加程序的内存占用。为了防止内存占用过多影响到程序的正常运行就需要对内存进行管理。 移动端的内存管理机制&#xff0c;主要有三种&#xff1a; 自动垃圾收集(GC)手…

两台电脑之间如何进行数据传输?两台电脑数据传输攻略

在数字化时代&#xff0c;电脑之间的数据传输变得日益重要。无论是个人用户还是企业用户&#xff0c;经常需要在不同的电脑之间共享或迁移数据。那么&#xff0c;两台电脑之间如何进行数据传输呢&#xff1f;本文将详细介绍两台电脑之间进行数据传输的几种常见方法&#xff0c;…

CI/CD的node.js编译报错npm ERR! network request to https://registry.npmjs.org/

1、背景&#xff1a; 在维护paas云平台过程中&#xff0c;有研发反馈paas云平台上的CI/CD的前端流水线执行异常。 2、问题描述&#xff1a; 流水线执行的是前端编译&#xff0c;使用的是node.js环境。报错内容如下&#xff1a; 2024-07-18T01:23:04.203585287Z npm ERR! code E…

【BUG】已解决:note: This is an issue with the package mentioned above,not pip.

已解决&#xff1a;note: This is an issue with the package mentioned above&#xff0c;not pip. 欢迎来到英杰社区https://bbs.csdn.net/topics/617804998 欢迎来到我的主页&#xff0c;我是博主英杰&#xff0c;211科班出身&#xff0c;就职于医疗科技公司&#xff0c;热衷…

Pytorch基础应用

1.数据加载 1.1 读取文本文件 方法一&#xff1a;使用 open() 函数和 read() 方法 # 打开文件并读取全部内容 file_path example.txt # 替换为你的文件路径 with open(file_path, r) as file:content file.read()print(content)方法二&#xff1a;逐行读取文件内容 # 逐…

【 FPGA 线下免费体验馆】高端 AMD- xilinx 16 nm UltraScale +系列

在复杂的FPGA 开发的过程中&#xff0c;需要能够满足高速、高精度、多通道等的复杂应用。而一个简单的 FPGA 开发板是不具备这些的&#xff0c;因此需要用更高端&#xff0c;大容量&#xff0c;高速IO的 FPGA 芯片与其他硬件组成一个完整的系统开发。这里就产生了高端 FPGA 开发…

redis服务器同 redis 集群

搭建redis服务器 修改服务运行参数 常用命令常用命令 创建redis集群 准备做集群的主机&#xff0c;不允许存储数据、不允许设置连接密码 配置服务器&#xff1a; 1、在任意一台redis服务器上都可以执行创建集群的命令。 2、--cluster-replicas 1 给每个master服务器分配1台…

【go】Excelize处理excel表 带合并单元格、自动换行与固定列宽的文件导出

文章目录 1 简介2 相关需求与实现2.1 导出带单元格合并的excel文件2.2 导出增加自动换行和固定列宽的excel文件 1 简介 之前整理过使用Excelize导出原始excel文件与增加数据校验的excel导出。【go】Excelize处理excel表 带数据校验的文件导出 本文整理使用Excelize导出带单元…

汇编教程1

本教程主要教大家如何使用vscode插件编写汇编语言&#xff0c;这样更方便&#xff0c;不用在32位虚拟机中编写汇编语言&#xff0c;后续的汇编实验代码都是使用vscode编写&#xff0c;话不多说&#xff0c;开始教学 安装vscode 如果已经安装过vscode&#xff0c;可以跳过这一…

Python创建Excel表和读取Excel表的基础操作

下载openpyxl第三方库 winr打开命令行输入cmd 这个如果不行可以试试其他方法&#xff0c;在运行Python代码的软件里也有直接下载的地方&#xff0c;可以上网搜索 创建Excel表 示例代码&#xff1a;最后要记得保存&#xff0c;可以加一句提示语句。 import openpyxl lst[100,…

【Apache Doris】周FAQ集锦:第 16 期

【Apache Doris】周FAQ集锦&#xff1a;第 16 期 SQL问题数据操作问题运维常见问题其它问题关于社区 欢迎查阅本周的 Apache Doris 社区 FAQ 栏目&#xff01; 在这个栏目中&#xff0c;每周将筛选社区反馈的热门问题和话题&#xff0c;重点回答并进行深入探讨。旨在为广大用户…

简单工厂、工厂方法与抽象工厂之间的区别

简单工厂、工厂方法与抽象工厂之间的区别 1、简单工厂&#xff08;Simple Factory&#xff09;1.1 定义1.2 特点1.3 示例场景 2、工厂方法&#xff08;Factory Method&#xff09;2.1 定义2.2 特点2.3 示例场景 3、抽象工厂&#xff08;Abstract Factory&#xff09;3.1 定义3.…

【JavaEE-多线程背景-线程等待-线程的六种状态-线程安全问题-详解】

&#x1f308;个人主页&#xff1a;SKY-30 ⛅个人推荐&#xff1a;基于java提供的ArrayList实现的扑克牌游戏 |C贪吃蛇详解 ⚡学好数据结构&#xff0c;刷题刻不容缓&#xff1a;点击一起刷题 &#x1f319;心灵鸡汤&#xff1a;总有人要赢&#xff0c;为什么不能是我呢 &…

C语言实现冒泡排序

冒泡排序是一种简单的排序算法&#xff0c;它重复地遍历要排序的数列&#xff0c;一次比较两个元素&#xff0c;如果它们的顺序错误就把它们交换过来。 遍历数列的工作是重复地进行直到没有再需要交换&#xff0c;也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元…

数据包的跨层封装

首先&#xff0c;我们先简单地分析一下数据包的组成结构&#xff1a; 如图 数据包简略地分为以下几层&#xff1a; 二层&#xff1a;封装MAC地址&#xff08;数据链路层&#xff09; 三层&#xff1a;封装IP地址 — 表明源IP和目标IP&#xff0c;主要用于路由器之间的信息转发…

【人工智能】Transformers之Pipeline(三):文本转音频(text-to-audio/text-to-speech)

​​​​​​​ 一、引言 pipeline&#xff08;管道&#xff09;是huggingface transformers库中一种极简方式使用大模型推理的抽象&#xff0c;将所有大模型分为音频&#xff08;Audio&#xff09;、计算机视觉&#xff08;Computer vision&#xff09;、自然语言处理&#x…

挖矿宝藏之硬盘分区

目录 一、硬盘分区的相关知识 二、主分区、活动分区、扩展分区、逻辑盘和盘符 三、硬盘分区原因 1.减少硬盘空间的浪费 2.便于文件的分类管理 3.有利于病毒的防治 四、硬盘分区的原则 1.方便性 2.实用性 3.安全性 五、利用Diskpart进行分区 1.命令行工具Diskpart …