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

简介: 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,或者摄像头数据。

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

image.gifandroid-unity推送-20240117-修正.jpg

先说打开摄像头:

publicIEnumeratorInitCameraCor()
    {
// 请求权限yieldreturnApplication.RequestUserAuthorization(UserAuthorization.WebCam);
if (Application.HasUserAuthorization(UserAuthorization.WebCam) &&WebCamTexture.devices.Length>0)
        {
// 创建相机贴图web_cam_texture_=newWebCamTexture(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();
        }
    }

image.gif

前后摄像头切换

privatevoidSwitchCamera()
    {
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_=newWebCamTexture(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();
    }

image.gif

启动|停止RTMP

privatevoidOnPusherBtnClicked()
    {
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        {
boolis_started=StartRtmpPusher();
if(is_started)
            {
btn_pusher_.GetComponentInChildren<Text>().text="停止RTMP";
if(!is_rtsp_publisher_running_)
                {
StartCaptureAvData();
coroutine_=StartCoroutine(OnPostVideo());
                }
            }
        }
    }

image.gif

推送RTMP实现如下:

publicboolStartRtmpPusher()
    {
if (is_pushing_rtmp_)
        {
Debug.Log("已推送..");   
returnfalse;
        }
//获取输入框的urlstringurl=input_url_.text.Trim();
if (!is_rtsp_publisher_running_)
        {
InitAndSetConfig();
        }
if (pusher_handle_==0) {
Debug.LogError("StartRtmpPusher, publisherHandle is null..");
returnfalse;
        }
NT_PB_U3D_SetPushUrl(pusher_handle_, rtmp_push_url_);
intis_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..");
returnfalse;
        }
returntrue;
    }

image.gif

对应的InitAndSetConfig()实现如下:

privatevoidInitAndSetConfig()
    {
if ( java_obj_cur_activity_==null )
        {
Debug.LogError("getApplicationContext is null");
return;
        }
intaudio_opt=1;
intvideo_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;
        }
intfps=30;
intgop=fps*2;
if(video_encoder_type_== (int)PB_VIDEO_ENCODER_TYPE.VIDEO_ENCODER_HARDWARE_AVC)
        {
inth264HWKbps=setHardwareEncoderKbps(true, video_width_, video_height_);
h264HWKbps=h264HWKbps*fps/25;
Debug.Log("h264HWKbps: "+h264HWKbps);
intisSupportH264HWEncoder=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!");
            }
        }
elseif(video_encoder_type_== (int)PB_VIDEO_ENCODER_TYPE.VIDEO_ENCODER_HARDWARE_HEVC)
        {
inthevcHWKbps=setHardwareEncoderKbps(false, video_width_, video_height_);
hevcHWKbps=hevcHWKbps*fps/25;
Debug.Log("hevcHWKbps: "+hevcHWKbps);
intisSupportHevcHWEncoder=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            {
intis_enable_vbr=1;
intvideo_quality=CalVideoQuality(video_width_, video_height_, true);
intvbr_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);
        }
    }

image.gif

数据投递

Color32[] cam_texture=web_cam_texture_.GetPixels32();
introwStride=web_cam_texture_.width*4;
intlength=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);

image.gif

停止RTMP推送

privatevoidStopRtmpPusher()
    {
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;
    }

image.gif

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

总结

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

相关文章
|
3月前
|
开发框架 前端开发 Android开发
Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势
本文深入探讨了 Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势。这对于实现高效的跨平台移动应用开发具有重要指导意义。
362 4
|
17天前
|
前端开发 Java Shell
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
124 20
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
|
4月前
|
编解码 vr&ar 图形学
Unity下如何实现低延迟的全景RTMP|RTSP流渲染
随着虚拟现实技术的发展,全景视频成为新的媒体形式。本文详细介绍了如何在Unity中实现低延迟的全景RTMP或RTSP流渲染,包括环境准备、引入依赖、初始化客户端、解码与渲染、优化低延迟等步骤,并提供了具体的代码示例。适用于远程教育、虚拟旅游等实时交互场景。
122 5
|
3月前
|
编解码 vr&ar 图形学
Unity下如何实现低延迟的全景RTMP|RTSP流渲染
随着虚拟现实技术的发展,全景视频逐渐成为新的媒体形式。本文详细介绍了如何在Unity中实现低延迟的全景RTMP或RTSP流渲染,包括环境准备、引入依赖、初始化客户端、解码与渲染、优化低延迟等步骤,并提供了具体的代码示例。适用于远程教育、虚拟旅游等实时交互场景。
66 2
|
4月前
|
安全 Java 网络安全
Android远程连接和登录FTPS服务代码(commons.net库)
Android远程连接和登录FTPS服务代码(commons.net库)
61 1
|
4月前
|
存储 数据采集 分布式计算
Hadoop-17 Flume 介绍与环境配置 实机云服务器测试 分布式日志信息收集 海量数据 实时采集引擎 Source Channel Sink 串行复制负载均衡
Hadoop-17 Flume 介绍与环境配置 实机云服务器测试 分布式日志信息收集 海量数据 实时采集引擎 Source Channel Sink 串行复制负载均衡
83 1
|
4月前
|
存储 大数据 数据库
Android经典面试题之Intent传递数据大小为什么限制是1M?
在 Android 中,使用 Intent 传递数据时存在约 1MB 的大小限制,这是由于 Binder 机制的事务缓冲区限制、Intent 的设计初衷以及内存消耗和性能问题所致。推荐使用文件存储、SharedPreferences、数据库存储或 ContentProvider 等方式传递大数据。
165 0
|
5天前
|
存储 机器学习/深度学习 人工智能
2025年阿里云GPU服务器租用价格、选型策略与应用场景详解
随着AI与高性能计算需求的增长,阿里云提供了多种GPU实例,如NVIDIA V100、A10、T4等,适配不同场景。2025年重点实例中,V100实例GN6v单月3830元起,适合大规模训练;A10实例GN7i单月3213.99元起,适用于混合负载。计费模式有按量付费和包年包月,后者成本更低。针对AI训练、图形渲染及轻量级推理等场景,推荐不同配置以优化成本和性能。阿里云还提供抢占式实例、ESSD云盘等资源优化策略,支持eRDMA网络加速和倚天ARM架构,助力企业在2025年实现智能计算的效率与成本最优平衡。 (该简介为原文内容的高度概括,符合要求的字符限制。)
|
7天前
|
存储 弹性计算 人工智能
2025年阿里云企业云服务器ECS选购与配置全攻略
本文介绍了阿里云服务器的核心配置选择方法论,涵盖算力需求分析、网络与存储设计、地域部署策略三大维度。针对不同业务场景,如初创企业官网和AI模型训练平台,提供了具体配置方案。同时,详细讲解了购买操作指南及长期运维优化建议,帮助用户快速实现业务上云并确保高效运行。访问阿里云官方资源聚合平台可获取更多最新产品动态和技术支持。
|
9天前
|
弹性计算 JavaScript 前端开发
一键安装!阿里云新功能部署Nodejs环境到ECS竟然如此简单!
Node.js 是一种高效的 JavaScript 运行环境,基于 Chrome V8 引擎,支持在服务器端运行 JavaScript 代码。本文介绍如何在阿里云上一键部署 Node.js 环境,无需繁琐配置,轻松上手。前提条件包括 ECS 实例运行中且操作系统为 CentOS、Ubuntu 等。功能特点为一键安装和稳定性好,支持常用 LTS 版本。安装步骤简单:登录阿里云控制台,选择扩展程序管理页面,安装 Node.js 扩展,选择实例和版本,等待创建完成并验证安装成功。通过阿里云的公共扩展,初学者和经验丰富的开发者都能快速进入开发状态,开启高效开发之旅。