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://xiahunao.cn/news/3249670.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

设计分享—国外网站设计赏析

今天还是给大家分享一些国外的网站设计案例&#xff5e; 蓝蓝设计是一家专注而深入的界面设计公司&#xff0c;为期望卓越的国内外企业提供卓越的大数据可视化界面设计、B端界面设计、桌面端界面设计、APP界面设计、图标定制、用户体验设计、交互设计、UI咨询、高端网站设计、平…

来参与“向日葵杯”全国教育仿真技术大赛~

可点击进行了解&#xff1a;“向日葵杯”全国教育仿真技术大赛 (sunmooc.cn) 本次大赛共分为四个赛道&#xff1a;自主命题赛道、教育知识图谱设计赛道、FPGA硬件扑克牌对抗赛道、EasyAR元宇宙空间设计赛道。 参赛对象 &#xff1a; 具有正式学籍的在校研究生&#xff0c;本科…

Memcached介绍与使用

引言 本文是笔者对Memcached这个高性能分布式缓存组件的实践案例&#xff0c;Memcached是一种高性能的分布式内存对象缓存系统&#xff0c;用于减轻数据库负载&#xff0c;加速动态Web应用&#xff0c;提高网站访问速度。它通过在内存中缓存数据和对象来减少读取数据库的次数&…

Postman导出excel文件

0 写在前面 在我们后端写接口的时候&#xff0c;前端页面还没有出来&#xff0c;我们就得先接口测试&#xff0c;在此记录下如何使用postman测试导出excel接口。 如果不会使用接口传参可以看我这篇博客如何使用Postman 1 方法一 2 方法二 3 写在末尾 虽然在代码中写入文件名…

NVIDIA Container Toolkit 安装与配置帮助文档(Ubuntu,Docker)

NVIDIA Container Toolkit 安装与配置帮助文档(Ubuntu,Docker) 本文档详细介绍了在 Ubuntu Server 22.04 上使用 Docker 安装和配置 NVIDIA Container Toolkit 的过程。 概述 NVIDIA 容器工具包使用户能够构建和运行 GPU 加速容器。即可以在容器中使用NVIDIA显卡。 架构图如…

uniapp H5 如何根据接口返回form表单,跳转银联支付界面?

uniapp如何根据form表单&#xff0c;唤醒第三方支付? 文章目录 uniapp如何根据form表单&#xff0c;唤醒第三方支付?效果图实现 效果图 接口返回 form 表单数据 实现 // 例请求成功&#xff0c;返回数据 rechargePay({}).then(res > {// 接收接口返回数据let { result …

= null 和 is null;SQL中关于NULL处理的4个陷阱;三值逻辑

一、概述 1、NULL参与的所有的比较和算术运算符(>,,<,<>,<,>,,-,*,/) 结果为unknown&#xff1b; 2、unknown的逻辑运算(AND、OR、NOT&#xff09;遵循三值运算的真值表&#xff1b; 3、如果运算结果直接返回用户&#xff0c;使用NULL来标识unknown 4、如…

JRT实体视图查询

JRT的设计目标就是多数据库支持&#xff0c;对于爬行周边数据提供DolerGet解决爬取多维数据问题。但是对于通过父表字段筛选子表数据就不能通过DolerGet取数据了&#xff0c;因为查询到的父表数据没有子表数据的ID。 比如下面表&#xff1a; 我需要按登记号查询这个登记号的报…

【linux】服务器安装NVIDIA驱动

【linux】服务器安装NVIDIA驱动 【创作不易&#xff0c;求点赞关注收藏】&#x1f600; 文章目录 【linux】服务器安装NVIDIA驱动一、关闭系统自带驱动nouveau二、下载英伟达驱动三、安装英伟达驱动1、禁用X服务器和相关进程2、在TTY终端安装驱动3、验证是否安装成功4、重新启…

接口开发:Orcal数据库的批量修改sql

场景&#xff1a;在日常的CURD中一定会用到批量修改。在我们的项目中&#xff0c;使用的数据库是Orcal&#xff0c;由于之前基本都是使用Mysql的&#xff0c;使用的sql语句也基本都是用mysql的。但是在这次的接口编写时用mysql的批量修改出了问题&#xff0c;刚开始我还以为是写…

源码分析SpringCloud Gateway如何加载断言(predicates)与过滤器(filters)

我们今天的主角是Gateway网关&#xff0c;一听名字就知道它基本的任务就是去分发路由。根据不同的指定名称去请求各个服务&#xff0c;下面是Gateway官方的解释&#xff1a; Spring Cloud Gateway&#xff0c;其他的博主就不多说了&#xff0c;大家多去官网看看&#xff0c;只…

WDL(Wide Deep Learning for Recommender Systems)——Google经典CTR预估模型

一、文章简介 Wide & Deep Learning for Recommender Systems这篇文章介绍了一种结合宽线性模型和深度神经网络的方法&#xff0c;以实现推荐系统中的记忆和泛化。这种方法在Google Play商店的应用推荐系统中进行了评估&#xff0c;展示了其显著的性能提升。 推荐系统中的…

食堂采购系统开发:从需求分析到上线实施的完整指南

本篇文章&#xff0c;笔者将详细介绍食堂采购系统从需求分析到上线实施的完整过程&#xff0c;旨在为开发团队和管理者提供一个系统化的指南。 一、需求分析 1.用户需求 常见的需求包括&#xff1a; -采购计划管理 -供应商管理 -库存管理 -成本控制 -报表生成 2.系统功…

RK3568笔记四十:设备树

若该文为原创文章&#xff0c;转载请注明原文出处。 一、介绍 设备树 (Device Tree) 的作用就是描述一个硬件平台的硬件资源&#xff0c;一般描述那些不能动态探测到的设备&#xff0c;可以被动态探测到的设备是不需要描述。 设备树可以被 bootloader(uboot) 传递到内核&#x…

CentOS6minimal安装nginx-1.26.1.tar.gz 笔记240718

CentOS6安装新版nginx 240718, CentOS6.1-minimal 安装 nginx-1.26.1.tar.gz 下载 nginx-1.26.1.tar.gz 的页面 : https://nginx.org/en/download.html 下载 nginx-1.26.1.tar.gz : https://nginx.org/download/nginx-1.26.1.tar.gz CentOS6.1已过期, 给它更换yum源, 将下面…

SpringCloud------Sentinel(微服务保护)

目录 雪崩问题 处理方式!!!技术选型 Sentinel 启动命令使用步骤引入依赖配置控制台地址 访问微服务触发监控 限流规则------故障预防流控模式流控效果 FeignClient整合Sentinel线程隔离-------故障处理线程池隔离和信号量隔离​编辑 两种方式优缺点设置方式 熔断降级-----…

简述乐观锁和悲观锁——Java

悲观锁和乐观锁 悲观就是任何事都认为会往坏处发生&#xff0c;乐观就是认为任何事都会往好处发生。 打个比方&#xff0c;假如一个公司里只有一台打印机&#xff0c;如果多个人同时打印文件&#xff0c;可能出现混乱的问题&#xff0c;他的资料打印在了我的资料上&#xff0…

【视频讲解】神经网络、Lasso回归、线性回归、随机森林、ARIMA股票价格时间序列预测|附代码数据

全文链接&#xff1a;https://tecdat.cn/?p37019 分析师&#xff1a;Haopeng Li 随着我国股票市场规模的不断扩大、制度的不断完善&#xff0c;它在金融市场中也成为了越来越不可或缺的一部分。 【视频讲解】神经网络、Lasso回归、线性回归、随机森林、ARIMA股票价格时间序列…

Unity动画系统(4)

6.3 动画系统高级1-1_哔哩哔哩_bilibili p333- 声音组件添加 using System.Collections; using System.Collections.Generic; using UnityEngine; public class RobotAnimationController : MonoBehaviour { [Header("平滑过渡时间")] [Range(0,3)] publ…

AI智能名片S2B2C商城小程序在社群去中心化管理中的应用与价值深度探索

摘要&#xff1a;随着互联网技术的飞速发展&#xff0c;社群经济作为一种新兴的商业模式&#xff0c;正逐渐成为企业与用户之间建立深度连接、促进商业增长的重要途径。本文深入探讨了AI智能名片S2B2C商城小程序在社群去中心化管理中的应用&#xff0c;通过详细分析社群去中心化…