GB/T28181-2016基于RTP的视音频数据封装和技术实现

221 阅读7分钟

首先我们先回顾下相关技术规范,看看基于RTP的音视频数据PS封装。

C.1 基于RTP的视音频数据PS封装

基于RTP的 PS封装首先按照ISO/IEC13818-1:2000将视音频流封装成PS包,再将PS包以负载的方式封装成 RTP包。

进行PS封装时,应将每个视频帧封装为一个PS包,且每个关键帧的PS包中应包含系统头(System Header)和 PSM(ProgramStream Map),系统头和 PSM放置于PS包头之后、第一个PES包之前。

其中 PESV为视频 PES包,PESA 为音频PES包;视频非关键帧的PS包结构中一般不包含系统头和 PSM。PS包中各部分的具体数据结构参见ISO/IEC 13818-1:2000中的相关描述。

图C.1典型的视频关键帧PS包结构系统头应包含对PS包中码流种类的描述,其中视频和音频的流ID(stream_id)取值如下:

  • a) 视频流ID:0xE0;
  • b) 音频流ID:0xC0。

针对本文档规定的几种视音频格式,PSM 中流类型(stream_type)的取值如下:

  • a) MPEG-4视频流:0x10;
  • b) H.264视频流:0x1B;
  • c) SVAC视频流:0x80;
  • d) G.711音频流:0x90;
  • e) G.722.1音频流:0x92;
  • f) G.723.1音频流:0x93;
  • g) G.729音频流:0x99;
  • h) SVAC音频流:0x9B。

PS包封装的其他具体技术规范详见ISO/IEC13818-1:2000。

PS包的 RTP封装格式参照IETFRFC2250,RTP的主要参数设置如下:

a) 负载类型(payloadtype):96;

b) 编码名称(encodingname):PS;

c) 时钟频率(clockrate):90kHz;

d) SDP描述中“m”字段的“media”项:video。

C.2 基于 RTP的视音频基本流封装

该方式直接将视音频数据以负载的方式封装成 RTP包。

C.2.1 MPEG-4视频流的 RTP封装

MPEG-4视频流的 RTP封装格式应符合IETFRFC3016协议中的相关规定。 MPEG-4视频流 RTP包的负载类型(PayloadType)标识号选定:从IETFRFC3551—2003表5 中的动态范围(96~127)中选择,建议定为97。

C.2.2 H.264视频流的 RTP封装

H.264的 RTP载荷格式应符合IETFRFC3984中的相关规定。

H.264视频流RTP包的负载类型(PayloadType)标识号选定:从IETFRFC3551—2003表5中的动态范围(96~127)中选择,建议定为98。

C.2.3 SVAC视频流的 RTP封装

SVAC视频流的 RTP载荷格式可参照IETFRFC3984中的相关规定。

SVAC视频流 RTP包的负载类型(Payload Type)标志号选定,从IETF RFC 3551-2003表5中的动态范围(96~127)中选择,建议定为99。

C.2.4 音频流的 RTP封装

语音比特流宜采用标准的 RTP协议进行打包。

在一个RTP包中,音频载荷数据应为整数个音频编码帧,且时间长度在20ms~180ms之间

音频载荷数据的 RTP封装参数如下:

a) G.711的主要参数

G.711A律语音编码 RTP包的负载类型(PayloadType)的参数规定如下(见IETFRFC3551— 2003中的表4):

  • 负载类型(PT):8;
  • 编码名称(encodingname):PCMA;
  • 时钟频率(clockrate):8kHz;
  • 通道数:1;
  • SDP描述中“m”字段的“media”项:audio。

b) SVAC音频的主要参数

SVAC语音编码 RTP包的负载类型(PayloadType)的参数规定如下:

  • 负载类型(PT):20;
  • 编码名称(encodingname):SVACA;
  • 时钟频率(clockrate):8kHz;
  • 通道数:1;
  • SDP描述中“m”字段的“media”项:audio。

c) G.723.1的主要参数

G.723.1语音编码 RTP包的负载类型(PayloadType)的参数规定参照IETFRFC3551—2003 表4中的 G.723,具体如下:

  • 负载类型(PT):4;
  • 编码名称(encodingname):G723;
  • 时钟频率(clockrate):8kHz;
  • 通道数:1;
  • SDP描述中“m”字段的“media”项:audio。

d) G.729的主要参数

G.729语音编码 RTP 包的负载类型(PayloadType)的参数规定如下(见IETFRFC3551— 2003中的表4):

  • 负载类型(PT):18;
  • 编码名称(encodingname):G729;
  • 时钟频率(clockrate):8kHz;
  • 通道数:1;
  • SDP描述中“m”字段的“media”项:audio。

e) G.722.1的主要参数

G.722.1语音编码 RTP包的负载类型(PayloadType)的参数规定参照IETFRFC3551—2003 表4中 G.722,具体如下:

  • 负载类型(PT):9;
  • 编码名称(encodingname):G722;
  • 时钟频率(clockrate):8kHz;
  • 通道数:1;
  • SDP描述中“m”字段的“media”项:audio。

技术实现

本文以Android平台为例,介绍下Android平台GB28181接入模块设计。实现不具备国标音视频能力的Android终端,通过平台注册接入到现有的GB/T28181—2016服务,可用于如智能监控、智慧零售、智慧教育、远程办公、生产运输、智慧交通、车载或执法记录仪等场景。Android终端除支持常规的音视频数据接入外,还可以支持移动设备位置(MobilePosition)订阅和通知、语音广播和语音对讲。

功能设计如下:

  • ​[视频格式]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/UDP设置;
  • 支持注册、注销,支持注册刷新及注册有效期设置;
  • 支持设备目录查询应答;
  • 支持心跳机制,支持心跳间隔、心跳检测次数设置;
  • 支持移动设备位置(MobilePosition)订阅和通知;
  • 支持国标GB/T28181—2016平台接入;
  • 支持语音广播及语音对讲;
  • [实时水印]支持动态文字水印、png水印;
  • [镜像]Android平台支持前置摄像头实时镜像功能;
  • [实时静音]支持实时静音/取消静音;
  • [实时快照]支持实时快照;
  • [降噪]支持环境音、手机干扰等引起的噪音降噪处理、自动增益、VAD检测;
  • [外部编码前视频数据对接]支持YUV数据对接;
  • [外部编码前音频数据对接]支持PCM对接;
  • [外部编码后视频数据对接]支持外部H.264数据对接;
  • [外部编码后音频数据对接]外部AAC数据对接;
  • 支持录像录像相关功能。​

Android平台GB28181设备接入端,在收到平台端的invite请求和ack确认后,完成基础的信令交互,进入媒体数据发送阶段:

收到Invite后,开始创建RTP Sender,并完成相关的参数设定:

/*
 * CameraPublishActivity.java
 * Github: https://github.com/daniulive/SmarterStreaming
 */
@Override
public void ntsOnInvitePlay(String deviceId, PlaySessionDescription session_des) {
  handler_.postDelayed(new Runnable() {
    @Override
    public void run() {
      MediaSessionDescription video_des = session_des_.getVideoDescription();
      SDPRtpMapAttribute ps_rtpmap_attr = video_des.getPSRtpMapAttribute();

      Log.i(TAG,"ntsInviteReceived, 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());

      // 可以先给信令服务器发送临时振铃响应
      //sip_stack_android.respondPlayInvite(180, device_id_);

      long rtp_sender_handle = libPublisher.CreateRTPSender(0);
      if ( rtp_sender_handle == 0 ) {
        gb28181_agent_.respondPlayInvite(488, device_id_);
        Log.i(TAG, "ntsInviteReceived 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();

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

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

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

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

      String local_ip_addr = IPAddrUtils.getIpAddress(context_);
      gb28181_agent_.respondPlayInviteOK(device_id_,local_ip_addr, local_port);

      gb28181_rtp_sender_handle_ = rtp_sender_handle;
    }

    private String device_id_;
    private PlaySessionDescription session_des_;

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

Ack后,开始发送数据:

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

      if (!isRecording && !isRTSPPublisherRunning && !isPushingRtmp) {
        InitAndSetConfig();
      }

      libPublisher.SetGB28181RTPSender(publisherHandle, gb28181_rtp_sender_handle_, gb28181_rtp_payload_type_, gb28181_rtp_encoding_name_);
      int startRet = libPublisher.StartGB28181MediaStream(publisherHandle);
      if (startRet != 0) {

        if (!isRecording && !isRTSPPublisherRunning && !isPushingRtmp ) {
          if (publisherHandle != 0) {
            libPublisher.SmartPublisherClose(publisherHandle);
            publisherHandle = 0;
          }
        }

        destoryRTPSender();

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

      if (!isRecording && !isRTSPPublisherRunning && !isPushingRtmp) {
        if (pushType == 0 || pushType == 1) {
          CheckInitAudioRecorder();    //enable pure video publisher..
        }
      }

      startLayerPostThread();

      isGB28181StreamRunning = true;
    }

    private String device_id_;

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

  }.set(deviceId),0);
}

通过信令和媒体数据交互分离,设备注册后,心跳机制保持在线状态,无需音视频数据编码,平台端如果需要查看实时媒体数据,发起invite请求,采集音视频数据,编码并实现RTP的视音频数据PS封装后实时传输,达到随看随传的目的。