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天前
|
移动开发 开发工具 Android开发
安卓与iOS开发环境对比:选择适合你的平台
【8月更文挑战第30天】 在移动应用开发的广阔天地中,安卓和iOS两大平台各领风骚。本文将深入浅出地分析这两个平台的开发环境,从工具、语言到用户群体等多个维度进行比较,旨在帮助开发者根据自己的技能和市场需求做出明智的选择。无论你是初涉移动开发领域的新手,还是寻求扩展技能边界的资深开发者,这篇文章都将为你提供有价值的见解和建议。
10 1
|
6天前
|
安全 Android开发 Swift
安卓与iOS开发:平台差异与技术选择
【8月更文挑战第26天】 在移动应用开发的广阔天地中,安卓和iOS两大平台各占一方。本文旨在探索这两个系统在开发过程中的不同之处,并分析开发者如何根据项目需求选择合适的技术栈。通过深入浅出的对比,我们将揭示各自平台的优势与挑战,帮助开发者做出更明智的决策。
|
3天前
|
Android开发
基于Amlogic 安卓9.0, 驱动简说(四):Platform平台驱动,驱动与设备的分离
本文介绍了如何在基于Amlogic T972的Android 9.0系统上使用Platform平台驱动框架和设备树(DTS),实现设备与驱动的分离,并通过静态枚举在设备树中描述设备,自动触发驱动程序的加载和设备创建。
4 0
基于Amlogic 安卓9.0, 驱动简说(四):Platform平台驱动,驱动与设备的分离
|
10天前
|
IDE 开发工具 Android开发
探索安卓与iOS开发的差异:平台选择对项目成功的影响
在移动应用开发的广阔天地中,安卓和iOS两大平台各领风骚,引领着技术进步的潮流。本文旨在深入剖析这两个平台在开发过程中的关键差异点,包括编程语言、开发工具、用户界面设计以及市场分布等方面。通过对比分析,我们不仅能更好地理解每个平台的独特优势,还能洞察这些差异如何影响项目决策和最终成果。无论你是开发者还是企业决策者,了解这些内容都将助你一臂之力,在选择适合自己项目的开发平台时做出更明智的决策。
|
9天前
|
IDE 开发工具 Android开发
探索iOS与安卓开发的差异:平台选择对项目成功的影响
【8月更文挑战第22天】在数字化时代,移动应用成为企业和个人展示创意、提供服务的重要工具。iOS和安卓作为两大主流平台,各自拥有独特的优势和限制。本文将深入探讨这两个平台在开发过程中的主要差异,以及这些差异如何影响项目规划、用户体验和市场策略。通过比较分析,旨在为开发者和企业决策者提供有价值的见解,帮助他们根据项目需求做出明智的平台选择。
|
1天前
|
监控 Java 开发工具
如何快速对接Android平台GB28181接入模块(SmartGBD)
大牛直播SDK推出的Android平台GB28181接入SDK(SmartGBD),可实现不具备国标音视频能力的 Android终端,通过平台注册接入到现有的GB/T28181—2016服务,可用于如执法记录仪、智能安全帽、智能监控、智慧零售、智慧教育、远程办公、明厨亮灶、智慧交通、智慧工地、雪亮工程、平安乡村、生产运输、车载终端等场景,可能是业内为数不多功能齐全性能优异的商业级水准GB28181接入SDK。
|
10天前
|
移动开发 开发工具 Android开发
安卓与iOS开发之巅:探索两大移动平台的核心技术与创新趋势
【8月更文挑战第22天】 在移动应用开发的浩瀚星海中,安卓和iOS犹如两颗璀璨的星辰,各自绽放着独特的光芒。本文将深入剖析这两大平台的技术架构、开发工具、生态系统以及未来发展趋势,带领读者领略它们的魅力所在。无论你是安卓的忠实拥趸,还是iOS的铁杆粉丝,亦或是中立的开发者,这篇文章都将为你揭示一个多元而精彩的移动开发世界。让我们一起踏上这场技术之旅,感受安卓与iOS之间的碰撞与融合,共同见证移动开发的未来。
|
18天前
|
图形学
小功能⭐️获取Unity游戏物体上,所挂载组件的名称
小功能⭐️获取Unity游戏物体上,所挂载组件的名称
|
3月前
|
存储 JSON 关系型数据库
【unity实战】制作unity数据保存和加载系统——大型游戏存储的最优解
【unity实战】制作unity数据保存和加载系统——大型游戏存储的最优解
71 2
|
3月前
|
图形学
【制作100个unity游戏之29】使用unity复刻经典游戏《愤怒的小鸟》(完结,附带项目源码)(上)
【制作100个unity游戏之29】使用unity复刻经典游戏《愤怒的小鸟》(完结,附带项目源码)
135 2
下一篇
云函数