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

相关文章
|
7月前
|
Android开发
安卓虚拟摄像头替换摄像头, 微信虚拟相机替换拍照,安卓免root虚拟摄像头
虚拟摄像头系统包含多个组件:主摄像头类、预览面板、驱动接口、DirectShow实现和管理类
|
7月前
|
Android开发 数据安全/隐私保护
手机微信虚拟视频聊天,安卓免root虚拟摄像头,免root虚拟hook相机
以上代码实现了一个完整的免root虚拟摄像头方案,通过Hook系统摄像头服务和微信视频通话接口
|
7月前
|
编解码 Java Android开发
安卓虚拟摄像头免root版,虚拟摄像头替换真实摄像头,jar代码开源分享
通过动态替换摄像头输入流的方式实现虚拟摄像头功能,代码经过简化展示核心逻辑。实际开发中还需要考虑视频编解码优化
|
7月前
|
Java Android开发
安卓虚拟摄像头过人脸,免root虚拟hook相机,虚拟相机hook版【jar】
两种Hook Android相机的方法:Xposed模块和Frida脚本。Xposed模块需要安装在已root的设备
|
7月前
|
监控 Android开发 数据安全/隐私保护
批量发送短信的平台,安卓群发短信工具插件脚本,批量群发短信软件【autojs版】
这个Auto.js脚本实现了完整的批量短信发送功能,包含联系人管理、短信内容编辑、发送状态监控等功能
|
7月前
|
API Android开发 数据安全/隐私保护
|
7月前
|
API 开发工具 Android开发
qq虚拟视频插件下载安装手机版, 安卓虚拟视频插件,替换摄像头工具
Xposed入口模块:拦截目标应用的相机调用‌23 Camera1 API处理:通过PreviewCallback替换视频流‌1 Camera2 API适
|
11月前
|
存储 编解码 监控
Android平台GB28181执法记录仪技术方案与实现
本文介绍了大牛直播SDK的SmartGBD在执法记录仪场景中的应用。GB28181协议作为视频监控联网的国家标准,为设备互联互通提供规范。SmartGBD专为Android平台设计,支持音视频采集、编码与传输,具备自适应算法和多功能扩展优势。文章分析了执法记录仪的需求,如实时音视频传输、设备管理及数据安全,并详细阐述了基于SmartGBD的技术实现方案,包括环境准备、SDK集成、设备注册、音视频处理及功能扩展等步骤。最后展望了SmartGBD在未来智慧物联领域的广阔应用前景。
699 13
|
11月前
|
存储 编解码 开发工具
Android平台毫秒级低延迟HTTP-FLV直播播放器技术探究与实现
本文详细探讨了在Android平台上实现HTTP-FLV播放器的过程。首先介绍了FLV格式的基础,包括文件头和标签结构。接着分析了HTTP-FLV传输原理,通过分块传输实现流畅播放。然后重点讲解了播放器的实现步骤,涵盖网络请求、数据解析、音视频解码与渲染,以及播放控制功能的设计。文章还讨论了性能优化和网络异常处理的方法,并总结了HTTP-FLV播放器的技术价值,尤其是在特定场景下的应用意义。
515 11
|
11月前
|
监控 Shell Linux
Android调试终极指南:ADB安装+多设备连接+ANR日志抓取全流程解析,覆盖环境变量配置/多设备调试/ANR日志分析全流程,附Win/Mac/Linux三平台解决方案
ADB(Android Debug Bridge)是安卓开发中的重要工具,用于连接电脑与安卓设备,实现文件传输、应用管理、日志抓取等功能。本文介绍了 ADB 的基本概念、安装配置及常用命令。包括:1) 基本命令如 `adb version` 和 `adb devices`;2) 权限操作如 `adb root` 和 `adb shell`;3) APK 操作如安装、卸载应用;4) 文件传输如 `adb push` 和 `adb pull`;5) 日志记录如 `adb logcat`;6) 系统信息获取如屏幕截图和录屏。通过这些功能,用户可高效调试和管理安卓设备。