当前位置:首页 » 《随便一记》 » 正文

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

28 人参与  2024年02月10日 15:56  分类 : 《随便一记》  评论

点击全文阅读


技术背景

我们在对接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,或者摄像头数据。

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

先说打开摄像头:

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

前后摄像头切换

    private void SwitchCamera()    {        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_ = new WebCamTexture(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

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

推送RTMP实现如下:

    public bool StartRtmpPusher()    {        if (is_pushing_rtmp_)        {            Debug.Log("已推送..");               return false;        }        //获取输入框的url        string url = input_url_.text.Trim();        if (!is_rtsp_publisher_running_)        {            InitAndSetConfig();        }        if (pusher_handle_ == 0) {             Debug.LogError("StartRtmpPusher, publisherHandle is null..");            return false;        }        NT_PB_U3D_SetPushUrl(pusher_handle_, rtmp_push_url_);        int is_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..");            return false;        }        return true;    }

对应的InitAndSetConfig()实现如下:

    private void InitAndSetConfig()    {        if ( java_obj_cur_activity_ == null )        {            Debug.LogError("getApplicationContext is null");            return;        }        int audio_opt = 1;        int video_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;        }        int fps = 30;        int gop = fps * 2;        if(video_encoder_type_ == (int)PB_VIDEO_ENCODER_TYPE.VIDEO_ENCODER_HARDWARE_AVC)        {            int h264HWKbps = setHardwareEncoderKbps(true, video_width_, video_height_);            h264HWKbps = h264HWKbps * fps / 25;            Debug.Log("h264HWKbps: " + h264HWKbps);            int isSupportH264HWEncoder = 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:CBR                NT_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 4                NT_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!");            }        }        else if(video_encoder_type_ == (int)PB_VIDEO_ENCODER_TYPE.VIDEO_ENCODER_HARDWARE_HEVC)        {            int hevcHWKbps = setHardwareEncoderKbps(false, video_width_, video_height_);            hevcHWKbps = hevcHWKbps*fps/25;            Debug.Log("hevcHWKbps: " + hevcHWKbps);            int isSupportHevcHWEncoder = 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:CBR                NT_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            {                int is_enable_vbr = 1;                int video_quality = CalVideoQuality(video_width_, video_height_, true);                int vbr_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();        int rowStride = web_cam_texture_.width * 4;        int length = 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推送

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


点击全文阅读


本文链接:http://zhangshiyu.com/post/68054.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1