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;}//获取输入框的urlstring 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:CBRNT_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 4NT_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:CBRNT_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.hqwc.cn/news/412084.html

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

相关文章

Tessy—嵌入式软件单元测试/集成测试工具

产品概述 Tessy源自戴姆勒—奔驰公司的软件技术实验室&#xff0c;由德国Hitex公司负责销售及技术的支持服务&#xff0c;是一款专门针对嵌入式软件进行单元/集成测试的工具。它可以对C/C代码进行单元、集成测试&#xff0c;可以自动化搭建测试环境、执行测试、评估测试结果并生…

[SS]语义分割——基础知识

语义分割前言 目录 一、定义 1、概念 2、 常见分割任务 3、建筑物提取(Building Footprint Extraction) 二、任务数据 1、数据集格式 2、结果具体形式 三、评价指标与标注 1、评价指标 2、标注工具 一、定义 1、概念 语义分割&#xff08;Semantic Segmentation&…

软件测试|使用Python轻松裁剪视频

简介 裁剪视频是在视频编辑和处理中常见的任务之一&#xff0c;Python提供了多种库和工具&#xff0c;可以用来裁剪视频。在本文中&#xff0c;我们将详细讨论如何使用Python来裁剪视频&#xff0c;并提供示例代码。 步骤1&#xff1a;环境准备 首先&#xff0c;我们要安装必…

Web3去中心化存储:重新定义云服务

随着Web3技术的崭露头角&#xff0c;去中心化存储正在成为数字时代云服务的全新范式。传统的云服务依赖于中心化的数据存储架构&#xff0c;而Web3的去中心化存储则为用户带来了更安全、更隐私、更可靠的数据管理方式&#xff0c;重新定义了云服务的未来。 1.摒弃中心化的弊端 …

IDEA 在本地启动多个 SpringBoot 后端服务模拟集群

目录 方式一&#xff1a;使用 IDEA 界面在多个后端端口运行同一个项目 方式二&#xff1a;通过控制台在运行项目 jar 包时传入端口配置 方式一&#xff1a;使用 IDEA 界面在多个后端端口运行同一个项目 1. 点击 Run / Debug 在默认端口启动项目 2. 点击 Services&#xff0…

群晖NAS搭建WebDav结合内网穿透实现公网访问本地影视资源

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

源码:Spring常规Bean创建过程

Bean创建过程&#xff1a; 一、版本 5.3.10二、学习内容 Bean创建过程源码三、Bean生命周期 时间轴地址&#xff1a;点击 四、bean创建过程脑图总结 脑图地址&#xff1a;点击 五、源码过程 说明&#xff1a; bean创建入口一般都是通过getBean(xxx);方法进入的&#xf…

【并发编程系列】putIfAbsent和getOrDefault用法

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

解决Windows下VSCode控制台乱码问题

我们在Windows使用VSCode编写C/C程序时&#xff0c;如果代码中的中文字符串使用的是UTF8编码&#xff0c;且代码内没有设置控制台的输出编码&#xff0c;或者编译时没有指定运行时编码&#xff08;GCC可以在编译时使用-fexec-charsetGBK来指定运行时的字符串编码&#xff1b;cl…

node.js(express.js)+mysql实现注册功能

文章目录 实现步骤一、获取客户端提交到服务器的用户信息&#xff0c;对表单中的数据&#xff0c;进行合法性的效验 代码如下:二、检测用户名是否被占用三、对密码进行加密四、插入新用户&#xff08;完整代码&#xff09;总结 实现步骤 一、获取客户端提交到服务器的用户信息…

PyTorch各种损失函数解析:深度学习模型优化的关键(2)

目录 详解pytorch中各种Loss functions mse_loss 用途 用法 使用技巧 注意事项 参数 数学理论公式 代码演示 margin_ranking_loss 用途 用法 使用技巧 注意事项 参数 数学理论公式 代码演示 multilabel_margin_loss 用途 用法 使用技巧 注意事项 参数 …

commvault学习(5):在linux上安装cv客户端

我的环境&#xff1a; 服务器&#xff08;同时装有CS、MA&#xff09;&#xff1a;windows server2008r2 客户端&#xff1a;两台centos7 1.为两台centos7配置静态ip 使得2者可以与服务器ping通 2.在两台centos7上预留出足够大的磁盘空间以存放安装文件 我是在/mnt下创建了…