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服务出来的数据,整体都在毫秒级延迟,感兴趣的开发者,可以跟我沟通交流测试。

相关文章
|
2月前
|
Web App开发 网络协议 Android开发
Android平台一对一音视频通话方案大比拼:WebRTC VS RTMP VS RTSP,谁才是王者?
【9月更文挑战第4天】本文详细对比了在Android平台上实现一对一音视频通话时常用的WebRTC、RTMP及RTSP三种技术方案。从技术原理、性能表现与开发难度等方面进行了深入分析,并提供了示例代码。WebRTC适合追求低延迟和高质量的场景,但开发成本较高;RTMP和RTSP则在简化开发流程的同时仍能保持较好的传输效果,适用于不同需求的应用场景。
127 1
|
2月前
|
安全 API 开发工具
Android平台RTMP推送|轻量级RTSP服务如何实现麦克风|扬声器声音采集切换
Android平台扬声器播放声音的采集,在无纸化同屏等场景下,意义很大,早期低版本的Android设备,是没法直接采集扬声器audio的(从Android 10开始支持),所以,如果需要采集扬声器audio,需要先做系统版本判断,添加相应的权限。
|
2月前
|
编解码 开发工具 Android开发
Android平台实现屏幕录制(屏幕投影)|音频播放采集|麦克风采集并推送RTMP或轻量级RTSP服务
Android平台屏幕采集、音频播放声音采集、麦克风采集编码打包推送到RTMP和轻量级RTSP服务的相关技术实现,做成高稳定低延迟的同屏系统,还需要有配套好的RTMP、RTSP直播播放器
|
15天前
|
存储 弹性计算 安全
阿里云第七代云服务器ECS性能、适用场景与价格参考
阿里云第七代云服务器ECS(Elastic Compute Service)作为阿里云最新一代的高性能计算产品,凭借其基于最新硬件架构和虚拟化技术的全面升级,在计算能力、存储性能、网络传输速度以及灵活性等多个方面实现了显著提升。这一代云服务器旨在为用户提供更为强大、稳定且可定制的云端基础设施服务,广泛适用于从基础的Web托管到复杂的高性能计算等多种应用场景。
|
14天前
|
弹性计算 网络安全
阿里云国际OpenAPI多接口快速管理ECS服务器教程
阿里云国际OpenAPI多接口快速管理ECS服务器教程
|
3天前
|
存储 弹性计算 NoSQL
"从入门到实践,全方位解析云服务器ECS的秘密——手把手教你轻松驾驭阿里云的强大计算力!"
【10月更文挑战第23天】云服务器ECS(Elastic Compute Service)是阿里云提供的基础云计算服务,允许用户在云端租用和管理虚拟服务器。ECS具有弹性伸缩、按需付费、简单易用等特点,适用于网站托管、数据库部署、大数据分析等多种场景。本文介绍ECS的基本概念、使用场景及快速上手指南。
17 3
|
8天前
|
存储 弹性计算 编解码
通过阿里云的活动租赁云服务器时如何选择实例规格?选择指南参考
新手用户通过阿里云的活动租赁云服务器的时候实例规格应该怎么选?目前在阿里云的活动中,可选的云服务器类型除了轻量应用服务器之外,云服务器的主要实例规格有经济型e、通用算力型u1和计算型c7与c8y、通用型g7与g8y、内存型r7与r8y等实例,但是对于新手来说,由于是初次购买,实例规格往往不知道怎么选择了。本文为大家展示阿里云目前活动中各云服务器实例规格性能、适用场景以及选择指南参考。
|
12天前
|
弹性计算 开发框架 .NET
阿里云服务器购买教程及云服务器地域、实例、操作系统、带宽等参数选择指南
对于初次购买阿里云服务器的用户来说,想使用阿里云服务器搭建网站或者运行APP、小程序等项目,第一步就是要先购买阿里云服务器,下面小编以图文形式给大家介绍一下阿里云服务器的购买流程,以及购买过程中如何云服务器地域、实例、带宽等关键配置和选择这些参数的一些注意事项,以供参考。
|
15天前
|
域名解析 网络协议 数据安全/隐私保护
阿里云轻量应用服务器搭建WordPress个人博客教程
阿里云轻量应用服务器搭建WordPress个人博客教程
|
19天前
|
存储 机器学习/深度学习 应用服务中间件
阿里云倚天云服务器实例:计算型c8y、通用型g8y、内存型r8y实例介绍
阿里云倚天云服务器是基于阿里云自研的倚天710 ARM架构CPU打造的高性能计算产品系列,它依托先进的第四代神龙架构,旨在为用户提供稳定可预期的超高效能体验。倚天云服务器在存储、网络性能及计算稳定性方面实现了显著提升,主要得益于其芯片级的快速路径加速技术。本文将深度解析阿里云倚天云服务器的计算型c8y、通用型g8y、内存型r8y实例,探讨其优势及适用场景,以供选择参考。