Android平台Unity下如何通过WebCamTexture采集摄像头数据并推送至RTMP服务器或轻量级RTSP服务

技术背景

我们在对接Unity下推送模块的时候,遇到这样的技术诉求,开发者希望在Android的Unity场景下,获取到前后摄像头的数据,并投递到RTMP服务器,实现低延迟的数据采集处理。

在此之前,我们已经有了非常成熟的RTMP推送模块,也实现了Android平台Unity环境下的Camera场景采集,针对这个技术需求,有两种解决方案:

1. 通过针对原生android camera接口封装,打开摄像头,并回调NV12|NV21数据,在Unity环境下渲染即可;

2. 通过WebCamTexture组件,通过系统接口,拿到数据,直接编码推送。

对于第一种方案,涉及到camera接口的二次封装和数据回调,也可以实现,但是不如WebCamTexture组件方便,本文主要介绍下方案2。

WebCamTexture

WebCamTexture继承自Texture,下面是官方资料介绍。

描述

WebCam Texture 是实时视频输入渲染到的纹理。

静态变量

devices返回可用设备列表。

变量

autoFocusPoint通过此属性可以设置/获取摄像机的自动焦点。仅在 Android 和 iOS 设备上有效。
deviceName设置此属性可指定要使用的设备的名称。
didUpdateThisFrame视频缓冲区是否更新了此帧?
isDepth如果纹理基于深度数据,则此属性为 true。
isPlaying返回摄像机当前是否正在运行。
requestedFPS设置摄像机设备的请求的帧率(以每秒帧数为单位)。
requestedHeight设置摄像机设备的请求的高度。
requestedWidth设置摄像机设备的请求的宽度。
videoRotationAngle返回一个顺时针角度(以度为单位),可以使用此角度旋转多边形以使摄像机内容以正确的方向显示。
videoVerticallyMirrored返回纹理图像是否垂直翻转。

构造函数

WebCamTexture创建 WebCamTexture。

公共函数

GetPixel返回坐标 (x, y) 上的像素颜色。
GetPixels获取像素颜色块。
GetPixels32返回原始格式的像素数据。
Pause暂停摄像机。
Play启动摄像机。
Stop停止摄像机。

技术实现

本文以大牛直播SDK的Unity下WebCamTexture采集推送为例,audio的话,可以采集麦克风,或者通过audioclip采集unity场景的audio,video数据的话,可以采集unity场景的camera,或者摄像头数据。

除此之外,还可以设置常规的编码参数,比如软、硬编码,帧率码率关键帧等。

先说打开摄像头:

    public IEnumerator InitCameraCor()
    {
        // 请求权限
        yield return Application.RequestUserAuthorization(UserAuthorization.WebCam);

        if (Application.HasUserAuthorization(UserAuthorization.WebCam) && WebCamTexture.devices.Length > 0)
        {
            // 创建相机贴图
            web_cam_texture_ = new WebCamTexture(WebCamTexture.devices[web_cam_index_].name, web_cam_width_, web_cam_height_, fps_);
            web_cam_raw_image_.texture = web_cam_texture_;
            web_cam_texture_.Play();

        }
    }

前后摄像头切换

    private void SwitchCamera()
    {
        if (WebCamTexture.devices.Length < 1)
            return;

        if (web_cam_texture_ != null && web_cam_texture_.isPlaying)
        {
            web_cam_raw_image_.enabled = false;
            web_cam_texture_.Stop();
            web_cam_texture_ = null;
        }

        web_cam_index_++;
        web_cam_index_ = web_cam_index_ % WebCamTexture.devices.Length;

        web_cam_texture_ = new WebCamTexture(WebCamTexture.devices[web_cam_index_].name, web_cam_width_, web_cam_height_, fps_);
        web_cam_raw_image_.texture = web_cam_texture_;
        web_cam_raw_image_.enabled = true;
        web_cam_texture_.Play();
    }

启动|停止RTMP

    private void OnPusherBtnClicked()
    {
        if (is_pushing_rtmp_)
        {
            if(!is_rtsp_publisher_running_)
            {
                StopCaptureAvData();

                if (coroutine_ != null) {
                    StopCoroutine(coroutine_);
                    coroutine_ = null;
                }
            }

            StopRtmpPusher();

            btn_pusher_.GetComponentInChildren<Text>().text = "推送RTMP";
        }
        else
        {
            bool is_started = StartRtmpPusher();

            if(is_started)
            {
                btn_pusher_.GetComponentInChildren<Text>().text = "停止RTMP";
            
                if(!is_rtsp_publisher_running_)
                {
                    StartCaptureAvData();
                    coroutine_ = StartCoroutine(OnPostVideo());
                }
            }
        }
    }

推送RTMP实现如下:

    public bool StartRtmpPusher()
    {
        if (is_pushing_rtmp_)
        {
            Debug.Log("已推送..");   
            return false;
        }

        //获取输入框的url
        string url = input_url_.text.Trim();

        if (!is_rtsp_publisher_running_)
        {
            InitAndSetConfig();
        }

        if (pusher_handle_ == 0) {
             Debug.LogError("StartRtmpPusher, publisherHandle is null..");
            return false;
        }

        NT_PB_U3D_SetPushUrl(pusher_handle_, rtmp_push_url_);

        int is_suc = NT_PB_U3D_StartPublisher(pusher_handle_);

        if (is_suc  == DANIULIVE_RETURN_OK)
        {
            Debug.Log("StartPublisher success..");          
            is_pushing_rtmp_ = true;
        }
        else
        {
            Debug.LogError("StartPublisher failed..");
            return false;
        }

        return true;
    }

对应的InitAndSetConfig()实现如下:

    private void InitAndSetConfig()
    {
        if ( java_obj_cur_activity_ == null )
        {
            Debug.LogError("getApplicationContext is null");
            return;
        }

        int audio_opt = 1;
        int video_opt = 3;

        video_width_ = camera_.pixelWidth;
        video_height_ = camera_.pixelHeight;

        pusher_handle_ = NT_PB_U3D_Open(audio_opt, video_opt, video_width_, video_height_);

        if (pusher_handle_ != 0){
            Debug.Log("NT_PB_U3D_Open success");
            NT_PB_U3D_Set_Game_Object(pusher_handle_, game_object_);
        }
        else
        {
            Debug.LogError("NT_PB_U3D_Open failed!");
            return;
        }

        int fps = 30;
        int gop = fps * 2;

        if(video_encoder_type_ == (int)PB_VIDEO_ENCODER_TYPE.VIDEO_ENCODER_HARDWARE_AVC)
        {
            int h264HWKbps = setHardwareEncoderKbps(true, video_width_, video_height_);
            h264HWKbps = h264HWKbps * fps / 25;

            Debug.Log("h264HWKbps: " + h264HWKbps);

            int isSupportH264HWEncoder = NT_PB_U3D_SetVideoHWEncoder(pusher_handle_, h264HWKbps);

            if (isSupportH264HWEncoder == 0) {
                NT_PB_U3D_SetNativeMediaNDK(pusher_handle_, 0);
                NT_PB_U3D_SetVideoHWEncoderBitrateMode(pusher_handle_, 1); // 0:CQ, 1:VBR, 2:CBR
                NT_PB_U3D_SetVideoHWEncoderQuality(pusher_handle_, 39);
                NT_PB_U3D_SetAVCHWEncoderProfile(pusher_handle_, 0x08); // 0x01: Baseline, 0x02: Main, 0x08: High

                // NT_PB_U3D_SetAVCHWEncoderLevel(pusher_handle_, 0x200); // Level 3.1
                // NT_PB_U3D_SetAVCHWEncoderLevel(pusher_handle_, 0x400); // Level 3.2
                // NT_PB_U3D_SetAVCHWEncoderLevel(pusher_handle_, 0x800); // Level 4
                NT_PB_U3D_SetAVCHWEncoderLevel(pusher_handle_, 0x1000); // Level 4.1 多数情况下,这个够用了
                //NT_PB_U3D_SetAVCHWEncoderLevel(pusher_handle_, 0x2000); // Level 4.2

                // NT_PB_U3D_SetVideoHWEncoderMaxBitrate(pusher_handle_, ((long)h264HWKbps)*1300);

                Debug.Log("Great, it supports h.264 hardware encoder!");
            }
        }
        else if(video_encoder_type_ == (int)PB_VIDEO_ENCODER_TYPE.VIDEO_ENCODER_HARDWARE_HEVC)
        {
            int hevcHWKbps = setHardwareEncoderKbps(false, video_width_, video_height_);
            hevcHWKbps = hevcHWKbps*fps/25;

            Debug.Log("hevcHWKbps: " + hevcHWKbps);

            int isSupportHevcHWEncoder = NT_PB_U3D_SetVideoHevcHWEncoder(pusher_handle_, hevcHWKbps);

            if (isSupportHevcHWEncoder == 0) {
                NT_PB_U3D_SetNativeMediaNDK(pusher_handle_, 0);
                NT_PB_U3D_SetVideoHWEncoderBitrateMode(pusher_handle_, 0); // 0:CQ, 1:VBR, 2:CBR
                NT_PB_U3D_SetVideoHWEncoderQuality(pusher_handle_, 39);

                // NT_PB_U3D_SetVideoHWEncoderMaxBitrate(pusher_handle_, ((long)hevcHWKbps)*1200);

                Debug.Log("Great, it supports hevc hardware encoder!");
            }
        }
        else 
        {
            if (is_sw_vbr_mode_) //H.264 software encoder
            {
                int is_enable_vbr = 1;
                int video_quality = CalVideoQuality(video_width_, video_height_, true);
                int vbr_max_bitrate = CalVbrMaxKBitRate(video_width_, video_height_);
                vbr_max_bitrate = vbr_max_bitrate * fps / 25;

                NT_PB_U3D_SetSwVBRMode(pusher_handle_, is_enable_vbr, video_quality, vbr_max_bitrate);
                //NT_PB_U3D_SetSWVideoEncoderSpeed(pusher_handle_, 2);
            }
        }

        NT_PB_U3D_SetAudioCodecType(pusher_handle_, 1);

        NT_PB_U3D_SetFPS(pusher_handle_, fps);

        NT_PB_U3D_SetGopInterval(pusher_handle_, gop);

        if (audio_push_type_ == (int)PB_AUDIO_OPTION.AUDIO_OPTION_MIC_EXTERNAL_PCM_MIXER
            || audio_push_type_ == (int)PB_AUDIO_OPTION.AUDIO_OPTION_TWO_EXTERNAL_PCM_MIXER)
        {
            NT_PB_U3D_SetAudioMix(pusher_handle_, 1);
        }
        else
        {
            NT_PB_U3D_SetAudioMix(pusher_handle_, 0);
        }
    }

数据投递

        Color32[] cam_texture = web_cam_texture_.GetPixels32();

        int rowStride = web_cam_texture_.width * 4;
        int length = rowStride * web_cam_texture_.height;

        NT_PB_U3D_OnCaptureVideoRGBA32Data(pusher_handle_, (long)Color32ArrayToIntptr(cam_texture), length, rowStride, web_cam_texture_.width, web_cam_texture_.height,
                                   1, 0, 0, 0, 0);

停止RTMP推送

    private void StopRtmpPusher()
    {
        if(!is_pushing_rtmp_)
            return;

        NT_PB_U3D_StopPublisher(pusher_handle_);

        if(!is_rtsp_publisher_running_)
        {
            NT_PB_U3D_Close(pusher_handle_);
            pusher_handle_ = 0;

            NT_PB_U3D_UnInit();
        }

        is_pushing_rtmp_ = false;
    }

轻量级RTSP服务的接口封装,之前blog已多次提到,这里不再赘述。

总结

Unity场景下采集摄像头数据并编码打包推送到RTMP服务器或轻量级RTSP服务,采集获取数据不麻烦,主要难点在于需要控制投递到原生模块的帧率,比如设置30帧,实际采集到的数据是50帧,需要均匀的处理数据投递,达到既流畅延迟又低。配合SmartPlayer播放测试,无论是RTMP推送还是轻量级RTSP服务出来的数据,整体都在毫秒级延迟,感兴趣的开发者,可以跟我沟通交流测试。

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

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

相关文章

密码产品推介 | 沃通安全电子签章系统(ES-1)

产品介绍 沃通安全电子签章系统&#xff08;ES-1&#xff09;是一款基于密码技术、完全自主研发的商用密码产品&#xff0c;严格遵循国家密码管理局制定的相关标准&#xff0c;可为企业和个人提供安全、合规的电子签章功能服务。产品的主要用途是为各类文书、合同、表单等电子…

Solana Mobile开启第二代Saga手机预售,怎么购买Solana Mobile?

PANews 1月17日消息&#xff0c;Solana Mobile官方宣布开启其第二代Saga手机&#xff08;Chapter 2&#xff09;的预售&#xff0c;预购押金为450美元&#xff0c;预计将于2025年上半年发货。同时&#xff0c;Chapter 2的发售将会包括推荐&#xff08;Referrals&#xff09;和积…

【Linux系列】在Pop!OS的启动器中添加自定义程序图标

文章目录 前言一、创建快捷方式二、快捷方式参数三、添加右键菜单和注册MIME 前言 无论是在Windows上&#xff0c;还是Linux&#xff0c;或者安卓这些我们常用的操作系统上&#xff0c;一些应用程序的快捷方式放在桌面或者启动器&#xff0c;只需要简单的点击就可以启动&#…

海思hi3516dv500陀螺仪防抖调试过程问题分析

主要看cat /proc/umap/motionfusion 1、陀螺仪配置&#xff0c;使用在线零偏 2、采集的陀螺仪数据 3、矫正之后的陀螺仪数据 4、效果异常的情况下确认 1、镜头视场角是否异常 2、陀螺仪方向标定是否正常&#xff0c;正常的情况下矫正之后的数据在0上下震动 3、确认在线零偏…

Python实现员工管理系统(Django页面版 ) 八

Hello 大家新年好。今天这篇博客是用来填补之前的登录系统的不足所遗留下来的坑点&#xff0c;你们知道的&#xff0c;我有坑是必补啊。 首先我留的第一个坑点不知道大家有没有注意到&#xff0c;当我们没并没有登录的时候&#xff0c;但是如果我们事先知道一些内部测试的网站路…

前端公共组件库优化

背景 前段时间入职了新公司后&#xff0c;做一些内部前端基建的工作&#xff0c;其中一个工作就是优化现有的frontend-common公共组件库。之前的组件库一直是以源码依赖的形式存在&#xff0c;即各个项目通过git submodule的方式将该仓库引入到各个项目中&#xff0c;作为一个…

Win32 字符串表达式计算

简单表达式计算实用类 支持的运算如表所示 运算符号释义例子加法1024512-减法1024-512*乘法1024*1024/除法1024/128^平方1024^2%取模(求余数)10%3(优先级左括号(1024512)*8)优先级右括号(1024512)*8 表达式示例: 表达式有效性备注2(2-7)*2*(8-2)/2有效1024^3有效1024的3次方…

头像空白问题

当用户没有设置头像时&#xff0c;我们可以使用用户名第一个字来当头像 主要涉及一个截取&#xff0c;截取字符串第一个字 变量名.charAt(0) 如果变量名为null或者undefine 那么就会报错 使用可选链操作符 &#xff1f; 当前面的值为nul或undefine时&#xff0c;就不会执行…

CSS||选择器

目录 作用 分类 基础选择器 标签选择器 ​编辑类选择器 id选择器 通配符选择器 作用 选择器&#xff08;选择符&#xff09;就是根据不同需求把不同的标签选出来这就是选择器的作用。 简单来说&#xff0c;就是选择标签用的。 选择器的使用一共分为两步&#xff1a; 1.…

代码随想录算法训练营第23天 | 669. 修剪二叉搜索树 + 108.将有序数组转换为二叉搜索树 + 538.把二叉搜索树转换为累加树

今日任务 669. 修剪二叉搜索树 108.将有序数组转换为二叉搜索树 538.把二叉搜索树转换为累加树 总结篇 669. 修剪二叉搜索树 - Medium 题目链接&#xff1a;力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 给你二叉搜索树的根节点 root &#xf…

代码随想录算法训练营29期|day 22 任务以及具体安排

235. 二叉搜索树的最近公共祖先 class Solution {public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {if(root null) return null;//向左遍历if(root.val > p.val && root.val > q.val){TreeNode left lowestCommonAncestor(roo…

MySQL表的基本插入查询操作详解

博学而笃志&#xff0c;切问而近思 文章目录 插入插入更新 替换查询全列查询指定列查询查询字段为表达式查询结果指定别名查询结果去重 WHERE 条件基本比较逻辑运算符使用LIKE进行模糊匹配使用IN进行多个值匹配 排序筛选分页结果更新数据删除数据截断表聚合函数COUNTSUMAVGMAXM…

C语言——atoi函数解析

目录 前言 atoi函数的介绍 atoi函数的使用 atoi函数的模拟实现 前言 对于atoi函数大家可能会有些陌生&#xff0c;不过当你选择并阅读到这里时&#xff0c;请往下阅读&#xff0c;我相信你能对atoi函数熟悉该函数的头文件为<stdlib.h> 或 <cstdlib> atoi函数的…

被遗忘在角落的RPA,成了提升AI Agent执行能力的天选神器

LLM&#xff08;Large Language Models&#xff09;刚爆发之时&#xff0c;很多人认为RPA要完了&#xff0c;自然语言交互API操作足以干掉任何UI自动化工具。 然而&#xff0c;大语言模型应用发展到AI Agent这一步&#xff0c;大家才发现API并不是万能的。Agent平台雨后春笋一…

【开源项目】经典开源项目实景三维数字孪生泰山

飞渡科技数字孪生文旅运营中心&#xff0c;基于文旅单位的运营管理、服务质量以及游客需求&#xff0c;通过数字孪生、AR/VR、大数据分析等技术&#xff0c;为景区打造虚实融合、超沉浸体验的专属虚拟数字场景&#xff0c;实现文旅领域的数据可视化、产业数字化以及智能化管理。…

Django Web开发(day4)——数据模型使用与填充网站数据(对数据库的基本操作)

本博客将会涉及: Django 数据模型的使用视频数据的导入admin 后台的使用 1、Django 数据模型的使用 在上一篇中完成了网站的数据模型的创建,在数据模型创建之后,Django 会为我们的数据模型创建一套数据库抽象的 API 接口,以供我们进行检索数据、创建数据、更新和修改数据…

vim 编辑器如何同时注释多行以及将多行进行空格

当然可以&#xff0c;以下是我对您的文字进行润色后的版本&#xff1a; 一、场景 YAML文件对空格的要求非常严格&#xff0c;因此在修改YAML时&#xff0c;我们可能需要批量添加空格。 二、操作步骤 请注意&#xff1a;您的所有操作都将以第一行为基准。也就是说&#xff0…

滚动菜单ListView

activity_main.xml <include layout"layout/title"/> 引用上章自定义标题栏 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"xmlns:app&qu…

BigeMap在Unity3d中的应用,助力数字孪生

1. 首先需要用到3个软件&#xff0c;unity&#xff0c;gis office 和 bigemap离线服务器 Unity下载地址:点击前往下载页面(Unity需要 Unity 2021.3.2f1之后的版本) Gis office下载地址:点击前往下载页面 Bigemap离线服务器 下载地址: 点击前往下载页面 Unity用于数字孪生项…

计算机系统基础知识揭秘:硬件、处理器和校验码

计算机系统基础知识揭秘&#xff1a;硬件、处理器和校验码 一、计算机系统基础知识的重要性二、计算机系统硬件2.1、内存和存储设备2.2、输入输出设备 三、中央处理器&#xff08;CPU&#xff09;3.1、运算器3.2、控制器3.3、寄存器组3.4、多核CPU 四、数据表示4.1、原码、反码…
最新文章