首先我们先回顾下相关技术规范,看看基于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)取值如下:
针对本文档规定的几种视音频格式,PSM 中流类型(stream_type)的取值如下:
PS包封装的其他具体技术规范详见ISO/IEC13818-1:2000。
PS包的 RTP封装格式参照IETFRFC2250,RTP的主要参数设置如下:
a) 负载类型(payloadtype):96;
b) 编码名称(encodingname):PS;
c) 时钟频率(clockrate):90kHz;
d) SDP描述中“m”字段的“media”项:video。
该方式直接将视音频数据以负载的方式封装成 RTP包。
MPEG-4视频流的 RTP封装格式应符合IETFRFC3016协议中的相关规定。 MPEG-4视频流 RTP包的负载类型(PayloadType)标识号选定:从IETFRFC3551—2003表5 中的动态范围(96~127)中选择,建议定为97。
H.264的 RTP载荷格式应符合IETFRFC3984中的相关规定。
H.264视频流RTP包的负载类型(PayloadType)标识号选定:从IETFRFC3551—2003表5中的动态范围(96~127)中选择,建议定为98。
SVAC视频流的 RTP载荷格式可参照IETFRFC3984中的相关规定。
SVAC视频流 RTP包的负载类型(Payload Type)标志号选定,从IETF RFC 3551-2003表5中的动态范围(96~127)中选择,建议定为99。
语音比特流宜采用标准的 RTP协议进行打包。
在一个RTP包中,音频载荷数据应为整数个音频编码帧,且时间长度在20ms~180ms之间。
音频载荷数据的 RTP封装参数如下:
a) G.711的主要参数
G.711A律语音编码 RTP包的负载类型(PayloadType)的参数规定如下(见IETFRFC3551— 2003中的表4):
b) SVAC音频的主要参数
SVAC语音编码 RTP包的负载类型(PayloadType)的参数规定如下:
c) G.723.1的主要参数
G.723.1语音编码 RTP包的负载类型(PayloadType)的参数规定参照IETFRFC3551—2003 表4中的 G.723,具体如下:
d) G.729的主要参数
G.729语音编码 RTP 包的负载类型(PayloadType)的参数规定如下(见IETFRFC3551— 2003中的表4):
e) G.722.1的主要参数
G.722.1语音编码 RTP包的负载类型(PayloadType)的参数规定参照IETFRFC3551—2003 表4中 G.722,具体如下:
本文以Android平台为例,介绍下Android平台GB28181接入模块设计。实现不具备国标音视频能力的Android终端,通过平台注册接入到现有的GB/T28181—2016服务,可用于如智能监控、智慧零售、智慧教育、远程办公、生产运输、智慧交通、车载或执法记录仪等场景。Android终端除支持常规的音视频数据接入外,还可以支持移动设备位置(MobilePosition)订阅和通知、语音广播和语音对讲。
功能设计如下:
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封装后实时传输,达到随看随传的目的。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。