技术背景
我们在对接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。 |
公共函数
技术实现
本文以大牛直播SDK的Unity下WebCamTexture采集推送为例,audio的话,可以采集麦克风,或者通过audioclip采集unity场景的audio,video数据的话,可以采集unity场景的camera,或者摄像头数据。
除此之外,还可以设置常规的编码参数,比如软、硬编码,帧率码率关键帧等。
先说打开摄像头:
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(); } }
前后摄像头切换
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(); }
启动|停止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()); } } } }
推送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; }
对应的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); } }
数据投递
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);
停止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; }
轻量级RTSP服务的接口封装,之前blog已多次提到,这里不再赘述。
总结
Unity场景下采集摄像头数据并编码打包推送到RTMP服务器或轻量级RTSP服务,采集获取数据不麻烦,主要难点在于需要控制投递到原生模块的帧率,比如设置30帧,实际采集到的数据是50帧,需要均匀的处理数据投递,达到既流畅延迟又低。配合SmartPlayer播放测试,无论是RTMP推送还是轻量级RTSP服务出来的数据,整体都在毫秒级延迟,感兴趣的开发者,可以跟我沟通交流测试。