Android平台GB28181实时回传流程和技术实现

​规范解读

GB28181 中的 "INVITE" 是会话初始协议(SIP)中的一种请求方法,主要用于邀请一个或多个参与者加入特定的会话。在 GB28181 标准中,"INVITE" 请求通常用于发起媒体流的传输请求。当一个设备想要接收来自另一个设备的媒体流时,它会向目标设备发送一个 "INVITE" 请求,其中包含了关于会话的描述信息,如媒体类型、编码格式、传输协议等。

以下是 GB28181 中 "INVITE" 请求的一些关键特点和作用:

一、发起会话

  1. 触发媒体流传输:"INVITE" 请求是启动媒体流传输的关键步骤。当源设备发送 "INVITE" 请求时,它向目标设备表明了希望建立一个媒体会话的意图。这个请求中包含了源设备支持的媒体格式、编码方式和传输协议等信息,以便目标设备能够确定是否可以满足这些要求并接受会话邀请。
  2. 携带会话描述信息:"INVITE" 请求中通常包含会话描述协议(SDP)信息,用于描述媒体流的特性。SDP 信息包括媒体类型(音频、视频或两者兼有)、编码格式、媒体的传输地址和端口等。目标设备通过解析 SDP 信息,可以了解源设备的媒体能力,并决定是否能够参与会话。

二、协商媒体参数

  1. **媒体能力协商:**在 GB28181 中,不同的设备可能具有不同的媒体处理能力。通过 "INVITE" 请求和响应的交互过程,可以进行媒体能力的协商。目标设备在接收到 "INVITE" 请求后,会检查自己的媒体能力,并根据源设备的要求进行相应的调整。如果目标设备无法满足源设备的要求,它可以返回一个错误响应,或者提出替代的媒体参数建议。
  2. 编码格式选择:"INVITE" 请求还可以用于协商媒体流的编码格式。不同的编码格式在压缩效率、图像质量和带宽需求等方面有所不同。通过协商,可以选择一种双方都支持的编码格式,以确保媒体流的正常传输和播放。

三、建立连接

  1. **确定传输路径:**一旦目标设备接受了 "INVITE" 请求,双方就可以开始建立媒体流的传输连接。这个过程涉及确定媒体流的传输协议(如 RTP/RTCP)、传输地址和端口等信息。根据 GB28181 标准,媒体流可以通过 IP 网络进行传输,使用 UDP 或 TCP 协议。
  2. **会话管理:**在媒体流传输过程中,"INVITE" 请求所建立的会话可以通过其他 SIP 方法进行管理,如 "BYE" 用于结束会话,"ACK" 用于确认会话的建立等。这些方法可以帮助设备在会话期间进行状态监测、错误处理和资源管理。

Android GB28181技术实现

本文以大牛直播SDK的GB28181设备接入模块为例,大牛直播SDK推出的Android平台GB28181接入SDK(SmartGBD),可实现不具备国标音视频能力的 Android终端,通过平台注册接入到现有的GB/T28181---2016服务,可用于如执法记录仪、智能安全帽、智能监控、智慧零售、智慧教育、远程办公、明厨亮灶、智慧交通、智慧工地、雪亮工程、平安乡村、生产运输、车载终端等场景。进入系统后,先启动GB28181,注册到平台,等待国标平台发起回传请求。

scss 复制代码
class ButtonGB28181AgentListener implements View.OnClickListener {
	public void onClick(View v) {
		record_executor_.cancel_tasks();

		stopRecordDownloads(true);
		stopPlaybacks(true);

		stopAudioPlayer();
		destoryRTPReceiver();

		gb_broadcast_source_id_ = null;
		gb_broadcast_target_id_ = null;
		btnGB28181AudioBroadcast.setText("GB28181语音广播");
		btnGB28181AudioBroadcast.setEnabled(false);

		stopGB28181Stream();
		destoryRTPSender();

		if (null == gb28181_agent_ ) {
			if( !initGB28181Agent() )
				return;
		}

		if (gb28181_agent_.isRunning()) {
			gb28181_agent_.terminateAllAudioBroadcasts(true);
			gb28181_agent_.terminateAllPlays(true);// 目前测试下来,发送BYE之后,有些服务器会立即发送INVITE,是否发送BYE根据实际情况看
			gb28181_agent_.stop();
			btnGB28181Agent.setText("启动GB28181");
		}
		else {
			record_executor_.cancel_tasks();
			initPlaybacks(null);
			initRecordDownloads(null);
			if ( gb28181_agent_.start() ) {
				btnGB28181Agent.setText("停止GB28181");
			}
		}
	}
}

GBSIPAgentPlayListener主要系GB28181的Invite、Ack、Bye等处理:

arduino 复制代码
public interface GBSIPAgentPlayListener {

    /*
     *收到s=Play的实时视音频点播
     */
    void ntsOnInvitePlay(String deviceId, SessionDescription sessionDescription);

    /*
     *发送play invite response 异常
     */
    void ntsOnPlayInviteResponseException(String deviceId, int statusCode, String errorInfo);

    /*
     * 收到CANCEL play INVITE请求
     */
    void ntsOnCancelPlay(String deviceId);

    /*
     * 收到Ack
     */
    void ntsOnAckPlay(String deviceId);

    /*
     * 收到Bye
     */
    void ntsOnByePlay(String deviceId);

    /*
     * 不是在收到BYE Message情况下, 终止Play
     */
    void ntsOnTerminatePlay(String deviceId);

    /*
     * Play会话对应的对话终止, 一般不会出发这个回调,目前只有在响应了200K, 但在64*T1时间后还没收到ACK,才可能会出发
    收到这个, 请做相关清理处理
    */
    void ntsOnPlayDialogTerminated(String deviceId);
}

平台发起回传请求时,ntsOnInvitePlay()来响应invite请求。

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

			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 = libPublisher.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();

			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_);

			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) ) {
				libPublisher.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);
}

@Override
public void ntsOnCancelPlay(String deviceId) {
	// 这里取消Play会话
	handler_.postDelayed(new Runnable() {
		@Override
		public void run() {
			Log.i(TAG, "ntsOnCancelPlay, deviceId=" + device_id_);

			destoryRTPSender();
		}

		private String device_id_;

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

	}.set(deviceId),0);
}

收到Ack后,ntsOnAckPlay()处理后续逻辑:

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

			InitAndSetConfig();

			stream_publisher_.SetGB28181RTPSender(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);

			boolean start_ret  = stream_publisher_.StartGB28181MediaStream();
			if (!start_ret) {
				stream_publisher_.try_release();
				destoryRTPSender();
				Log.e(TAG, "Failed to start GB28181 service..");
				return;
			}

			startAudioRecorder();
			startLayerPostThread();
		}

		private String device_id_;

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

	}.set(deviceId),0);
}

@Override
public void ntsOnPlayInviteResponseException(String deviceId, int statusCode, String errorInfo) {
	// 这里要释放掉响应的资源
	Log.i(TAG, "ntsOnPlayInviteResponseException, deviceId=" + deviceId + " statusCode=" +statusCode
	+ " errorInfo:" + errorInfo);

	handler_.postDelayed(new Runnable() {
		@Override
		public void run() {
			Log.i(TAG, "ntsOnPlayInviteResponseException, deviceId=" + device_id_);

			destoryRTPSender();
		}

		private String device_id_;

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

	}.set(deviceId),0);
}

总结

GB28181 中的 "INVITE" 请求在媒体流传输中起着至关重要的作用,它通过发起会话、协商媒体参数和建立连接等步骤,实现了设备之间的媒体通信。在实际应用中,需要根据具体的需求和场景,合理地使用 "INVITE" 请求和其他 SIP 方法,以确保媒体流的稳定传输和高质量播放。

相关推荐
音视频牛哥2 天前
RTMP、RTSP直播播放器的低延迟设计探讨
音视频开发·视频编码·直播
音视频牛哥6 天前
电脑共享同屏的几种方法分享
音视频开发·视频编码·直播
aqi008 天前
FFmpeg开发笔记(五十四)使用EasyPusher实现移动端的RTSP直播
android·ffmpeg·音视频·直播·流媒体
aqi009 天前
FFmpeg开发笔记(五十三)移动端的国产直播录制工具EasyPusher
android·ffmpeg·音视频·直播·流媒体
加油吧x青年10 天前
Web端开启直播技术方案分享
前端·webrtc·直播
aqi0022 天前
FFmpeg开发笔记(五十二)移动端的国产视频播放器GSYVideoPlayer
android·ffmpeg·音视频·直播·流媒体
aqi0023 天前
FFmpeg开发笔记(五十一)适合学习研究的几个音视频开源框架
ffmpeg·音视频·直播·流媒体
aqi001 个月前
FFmpeg开发笔记(五十)聊聊几种流媒体传输技术的前世今生
ffmpeg·音视频·直播·流媒体