Android平台GB28181设备接入侧如何实现按需打开视音频采集传输

GB/T28181规范

GB/T28181是中国国家标准,全称为《安全防范视频监控联网系统信息传输、交换、控制技术要求》,该标准规定了城市安全防范监控系统中视频监控联网系统的一般要求和架构,以及信息传输、交换、控制的技术要求。它主要应用于安防领域,为各种视频监控系统提供了一致的接口规范,使得不同厂商生产的视频监控设备可以相互兼容。规范规定了公共安全视频监控联网系统(以下简称"联网系统")的互联结构,传输、交换、控制的基本要求和安全性要求,以及控制、传输流程和协议接口等技术要求。适用于公共安全视频监控联网系统的方案设计、系统检测、验收以及与之相关的设备研发生产。其他视频监控联网系统可参照执行。目前已更新至GB/T28181-2022版。

为什么要开发Android平台GB28181接入模块

实际上,Android平台GB28181接入模块,主要目标是可实现不具备国标音视频能力的 Android终端,通过平台注册接入到现有的GB/T28181---2016服务,可用于如执法记录仪、智能安全帽、智能监控、智慧零售、智慧教育、远程办公、明厨亮灶、智慧交通、智慧工地、雪亮工程、平安乡村、生产运输、车载终端等场景。

Android终端除支持常规的音视频数据接入外,还可以支持移动设备位置(MobilePosition)订阅和通知、语音广播和语音对讲,历史视音频文件查询和下载,支持对接数据类型如下:

  1. 编码前数据(目前支持的有YV12/NV21/NV12/I420/RGB24/RGBA32/RGB565等数据类型),其中,Android平台前后摄像头数据,或者屏幕数据,或者Unity拿到的数据,均属编码前数据;
  2. 编码后数据(如无人机等264/HEVC数据,或者本地解析的MP4音视频数据);
  3. 拉取RTSP或RTMP流并接入至GB28181平台(比如其他IPC的RTSP流,可通过Android平台GB28181接入到国标平台)。

功能设计

实际上,我们在做Android平台GB28181设备接入模块之前,已经有非常成熟的视音频采集(屏幕、摄像头、外部音视频数据)、软硬编码、录像、快照、实时动态水印等技术储备,所以,GB28181设备接入,主要考虑的是信令和媒体流传输这块,考虑到设备性能和实际场景,我们信令和媒体传输设计是分离的,Android端GB28181设备接入侧注册到国标平台后,如果国标平台不需要查看前端设备数据,我们仅维持心跳(KeepAlive),需要查看的时候,我们再开摄像头、麦克风编码打包投递数据给平台侧,尽可能的减少性能消耗,这块在执法记录仪、智能安全帽等场景下,非常实用。

  • 视频格式H.264/H.265(Android H.265硬编码);
  • 音频格式G.711 A律、AAC;
  • 音量调节Android平台采集端支持实时音量调节;
  • H.264硬编码支持H.264特定机型硬编码;
  • H.265硬编码支持H.265特定机型硬编码;
  • 软硬编码参数配置支持gop间隔、帧率、bit-rate设置;
  • 软编码参数配置支持软编码profile、软编码速度、可变码率设置;
  • 支持纯视频、音视频PS打包传输;
  • 支持RTP OVER UDP和RTP OVER TCP被动模式(TCP媒体流传输客户端);
  • 支持信令通道网络传输协议TCP/UDP设置;
  • 支持注册、注销,支持注册刷新及注册有效期设置;
  • 支持设备目录查询应答;
  • 支持心跳机制,支持心跳间隔、心跳检测次数设置;
  • 支持移动设备位置(MobilePosition)订阅和通知;
  • 支持语音广播;
  • 支持语音对讲;
  • 支持历史视音频文件检索;
  • 支持历史视音频文件下载;
  • 支持云台控制和预置位查询;
  • 实时水印支持动态文字水印、png水印;
  • 镜像Android平台支持前置摄像头实时镜像功能;
  • 实时静音支持实时静音/取消静音;
  • 实时快照支持实时快照;
  • 降噪支持环境音、手机干扰等引起的噪音降噪处理、自动增益、VAD检测;
  • 外部编码前视频数据对接支持YUV数据对接;
  • 外部编码前音频数据对接支持PCM对接;
  • 外部编码后视频数据对接支持外部H.264数据对接;
  • 外部编码后音频数据对接外部AAC数据对接;
  • 扩展录像功能支持和录像模块组合使用,录像相关功能。

Android端如何实现后台视音频GB28181接入

后台采集摄像头和麦克风这块,不再赘述,基本做Andorid开发的,都能搞得定,需要注意的是,后台service推送,需要加入省电优化白名单,以免8.0及以上版本设备后台运行超过一分钟被自动停掉,6.0以上版本,需要动态获取权限:

scss 复制代码
if (Build.VERSION.SDK_INT >=26)
{
  if(!isIgnoringBatteryOptimizations())
  {
    gotoSettingIgnoringBatteryOptimizations();
  }
}

//6.0及以上版本,动态获取Audio权限
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
{
  RequestAudioPermission();
}

我们要做的就是选择分辨率、软硬编码等参数后,启动GB28181即可:

因为系后台服务,启动后,任务栏可以看到:

收到平台侧发来的Invite后,我们会调用try_preview_camera()来启动摄像头后台预览。

ini 复制代码
/*
 * BackgroudService.java
 * Author: daniusdk.com
 */
@Override
public void ntsOnInvitePlay(String deviceId, SessionDescription session_des) {
  handler_.postDelayed(new Runnable() {
    @Override
    public void run() {
      // 先振铃响应下
      gb28181_agent_.respondPlayInvite(180, device_id_);

      if (!try_preview_camera()) {
        gb28181_agent_.respondPlayInvite(488, device_id_);
        Log.i(TAG, "ntsOnInvitePlay try_preview_camera failed, response 488, device_id:" + device_id_);
        return;
      }

      MediaSessionDescription video_des = null;
      SDPRtpMapAttribute ps_rtpmap_attr = null;

      // 28181 视频使用PS打包
      Vector<MediaSessionDescription> video_des_list = session_des_.getVideoPSDescriptions();
      if (video_des_list != null && !video_des_list.isEmpty()) {
        for(MediaSessionDescription m : video_des_list) {
          if (m != null && m.isValidAddressType() && m.isHasAddress() ) {
            video_des = m;
            ps_rtpmap_attr = video_des.getPSRtpMapAttribute();
            break;
          }
        }
      }

      if (null == video_des) {
        gb28181_agent_.respondPlayInvite(488, device_id_);
        Log.i(TAG, "ntsOnInvitePlay get video description is null, response 488, device_id:" + device_id_);
        return;
      }

      if (null == ps_rtpmap_attr ) {
        gb28181_agent_.respondPlayInvite(488, device_id_);
        Log.i(TAG, "ntsOnInvitePlay get ps rtp map attribute is null, response 488, device_id:" + device_id_);
        return;
      }

      Log.i(TAG,"ntsOnInvitePlay, device_id:" +device_id_+", is_tcp:" + video_des.isRTPOverTCP()
            + " rtp_port:" + video_des.getPort() + " ssrc:" + video_des.getSSRC()
            + " address_type:" + video_des.getAddressType() + " address:" + video_des.getAddress());

      long rtp_sender_handle = lib_publisher_.CreateRTPSender(0);
      if ( rtp_sender_handle == 0 ) {
        gb28181_agent_.respondPlayInvite(488, device_id_);
        Log.i(TAG, "ntsOnInvitePlay CreateRTPSender failed, response 488, device_id:" + device_id_);
        return;
      }

      gb28181_rtp_payload_type_  = ps_rtpmap_attr.getPayloadType();
      gb28181_rtp_encoding_name_ =  ps_rtpmap_attr.getEncodingName();

      lib_publisher_.SetRTPSenderTransportProtocol(rtp_sender_handle, video_des.isRTPOverUDP()?0:1);
      lib_publisher_.SetRTPSenderIPAddressType(rtp_sender_handle, video_des.isIPv4()?0:1);
      lib_publisher_.SetRTPSenderLocalPort(rtp_sender_handle, 0);
      lib_publisher_.SetRTPSenderSSRC(rtp_sender_handle, video_des.getSSRC());
      lib_publisher_.SetRTPSenderSocketSendBuffer(rtp_sender_handle, 2*1024*1024); // 设置到2M
      lib_publisher_.SetRTPSenderClockRate(rtp_sender_handle, ps_rtpmap_attr.getClockRate());
      lib_publisher_.SetRTPSenderDestination(rtp_sender_handle, video_des.getAddress(), video_des.getPort());

      if ( lib_publisher_.InitRTPSender(rtp_sender_handle) != 0 ) {
        gb28181_agent_.respondPlayInvite(488, device_id_);
        lib_publisher_.DestoryRTPSender(rtp_sender_handle);
        return;
      }

      int local_port = lib_publisher_.GetRTPSenderLocalPort(rtp_sender_handle);
      if (local_port == 0) {
        gb28181_agent_.respondPlayInvite(488, device_id_);
        lib_publisher_.DestoryRTPSender(rtp_sender_handle);
        return;
      }

      Log.i(TAG,"get local_port:" + local_port);

      String local_ip_addr = IPAddrUtils.getIpAddress(context_);

      MediaSessionDescription local_video_des = new MediaSessionDescription(video_des.getType());

      local_video_des.addFormat(String.valueOf(ps_rtpmap_attr.getPayloadType()));
      local_video_des.addRtpMapAttribute(ps_rtpmap_attr);

      local_video_des.setAddressType(video_des.getAddressType());
      local_video_des.setAddress(local_ip_addr);
      local_video_des.setPort(local_port);

      local_video_des.setTransportProtocol(video_des.getTransportProtocol());
      local_video_des.setSSRC(video_des.getSSRC());

      if (!gb28181_agent_.respondPlayInviteOK(device_id_,local_video_des) ) {
        lib_publisher_.DestoryRTPSender(rtp_sender_handle);
        Log.e(TAG, "ntsOnInvitePlay call respondPlayInviteOK failed.");
        return;
      }

      gb28181_rtp_sender_handle_ = rtp_sender_handle;
    }

    private String device_id_;
    private SessionDescription session_des_;

    public Runnable set(String device_id, SessionDescription session_des) {
      this.device_id_ = device_id;
      this.session_des_ = session_des;
      return this;
    }
  }.set(deviceId, session_des),0);
}

try_preview_camera()实现如下:

kotlin 复制代码
private boolean try_preview_camera() {
     if (camera_ != null)
         return true;

    SurfaceHolder surface_holder = get_surface_holder();
    if (null == surface_holder) {
        Log.e(TAG, "try_preview_camera surface_holder is null");
        return false;
    }

     camera_ = open_camera(current_camera_type_);
     if (null == camera_) {
         Log.e(TAG, "try_preview_camera open_camera is null type:" + current_camera_type_);
         return false;
     }

    if (!start_camera_preview(surface_holder)) {
        release_camera();
        Log.i(TAG, "try_preview_camera start_camera_preview failed");
        return false;
    }

    return true;
 }

收到ack后,直接发送数据到国标平台侧即可

ini 复制代码
@Override
public void ntsOnAckPlay(String deviceId) {
  handler_.postDelayed(new Runnable() {
    @Override
    public void run() {
      Log.i(TAG,"ntsOnACKPlay, device_id:" +device_id_);

      InitAndSetConfig();

      lib_publisher_.SetGB28181RTPSender(publisher_handle_, gb28181_rtp_sender_handle_, gb28181_rtp_payload_type_, gb28181_rtp_encoding_name_);

      //libPublisher.SetGBTCPConnectTimeout(publisherHandle, 10*60*1000);
      //libPublisher.SetGBInitialTCPReconnectInterval(publisherHandle, 1000);
      //libPublisher.SetGBInitialTCPMaxReconnectAttempts(publisherHandle, 3);

      int startRet = lib_publisher_.StartGB28181MediaStream(publisher_handle_);
      if (startRet != 0) {
        if (publisher_handle_ != 0) {
          lib_publisher_.SmartPublisherClose(publisher_handle_);
          publisher_handle_ = 0;
        }

        destoryRTPSender();

        Log.e(TAG, "Failed to start GB28181 service..");
        return;
      }

      startAudioRecorder();

      startLayerPostThread();

      is_gb_stream_running_ = true;
    }

    private String device_id_;

    public Runnable set(String device_id) {
      this.device_id_ = device_id;
      return this;
    }

  }.set(deviceId),0);
}

关闭预览查看,处理bye信令:

typescript 复制代码
@Override
public void ntsOnByePlay(String deviceId) {
  handler_.postDelayed(new Runnable() {
    @Override
    public void run() {
      Log.i(TAG, "ntsOnByePlay, stop GB28181 media stream, deviceId=" + device_id_);

      stopGB28181Stream();
      destoryRTPSender();
    }

    private String device_id_;

    public Runnable set(String device_id) {
      this.device_id_ = device_id;
      return this;
    }

  }.set(deviceId),0);
}

其中stopGB28181Stream()需要关闭摄像头(对应release_camera())和麦克风(对应stopAudioRecorder()),确保只有国标平台测查看的时候,才开启摄像头,尽可能的减少性能损耗。

ini 复制代码
//停止GB28181 媒体流
private void stopGB28181Stream() {
  if (!is_gb_stream_running_)
    return;

  stopLayerPostThread();
  stopAudioRecorder();
  release_camera();

  is_gb_stream_running_ = false;
  lib_publisher_.StopGB28181MediaStream(publisher_handle_);

  if (publisher_handle_ != 0) {
    lib_publisher_.SmartPublisherClose(publisher_handle_);
    publisher_handle_ = 0;
  }
}

总结

以上是大概的流程,摄像头麦克风采集做到后台的话,可以在需要预览采集数据的时候才打开,不用的时候,直接关闭,只保留信令这块,打开视音频预览后,如果有语音广播过来,可以直接播放语音广播的数据,这样尽可能的减少设备的性能消耗,提高待机时间,特别是执法记录仪等户外设备,按需打开摄像头和麦克风,按需投递视音频数据到平台外侧,意义非常大。

相关推荐
IT_陈寒2 小时前
SpringBoot这个自动配置坑我跳了三次
前端·人工智能·后端
Yeyu3 小时前
刷新一帧的艺术:invalidate / postInvalidate / postInvalidateOnAnimation全解析
android
用户395240998803 小时前
排坑日记:ASP.NET Core 中 "Required field is not provided" 验证错误全记录
后端
用户8356290780514 小时前
使用 Python 自动化 PowerPoint 形状布局与格式设置
后端·python
Oneslide4 小时前
sudo免密权限配置不生效
后端
潘潘潘4 小时前
Android OTA 升级原理和流程介绍
android
站大爷IP5 小时前
为什么Python不用var或let声明变量?
后端
赴星半途5 小时前
NestJS实战-创建AuthService
后端
北冥有鱼5 小时前
mqtt 测试
前端·后端