上篇文章提到Android端GB28181接入端的语音广播和语音对讲的实现,从spec角度大概介绍了下流程和简单的接口设计,好多开发者私信我,希望展开说一下。其实这块难度不大,只是广播和对讲涉及到双向实现,如果之前没有相关的积累,从头实现麻烦一些而已。
语音广播的流程大家应该非常清楚了,简单来说,SIP服务器发送Broadcast语音广播命令到android接入端,接入端应答,在收到200 OK后,发送INVITE消息,Android接入端收到INVITE的200 OK响应后,回复ACK,开始读取并解析RTP包,然后对音频数据解码,输出到Android播放设备即可。
从DEMO来看,当有语音广播接入进来后,GB28181语音广播按钮会处于可用状态。
语音广播信令Listener如下:
package com.gb28181.ntsignalling; public interface GBSIPAgentListener { /* *收到语音广播通知 */ void ntsOnNotifyBroadcastCommand(String fromUserName, String fromUserNameAtDomain, String sn, String sourceID, String targetID); /* *需要准备接受语音广播的SDP内容 */ void ntsOnAudioBroadcast(String commandFromUserName, String commandFromUserNameAtDomain, String sourceID, String targetID); /* *音频广播, 发送Invite请求异常 */ void ntsOnInviteAudioBroadcastException(String sourceID, String targetID, String errorInfo); /* *音频广播, 等待Invite响应超时 */ void ntsOnInviteAudioBroadcastTimeout(String sourceID, String targetID); /* *音频广播, 收到Invite消息最终响应 */ void ntsOnInviteAudioBroadcastResponse(String sourceID, String targetID, int statusCode, PlaySessionDescription sessionDescription); /* * 音频广播, 收到BYE Message */ void ntsOnByeAudioBroadcast(String sourceID, String targetID); /* * 不是在收到BYE Message情况下, 终止音频广播 */ void ntsOnTerminateAudioBroadcast(String sourceID, String targetID); }
相关信令接口如下:
package com.gb28181.ntsignalling; public interface GBSIPAgent { /* *语音广播应答 */ void respondBroadcastCommand(String fromUserName, String fromUserNameAtDomain, String sn, String sourceID, String targetID, boolean result); /* *语音广播接收者发送Invite消息, rtp ssrc暂时由sdk生成 *@param addressType: ipv4:"IP4", ipv6:"IP6", 其他不支持, 填充SDP用 *@param localAddress: 本地IP地址, 填充SDP用 *@param localPort: 本地端口, 填充SDP用 *@param mediaTransportProtocol: 媒体传输协议, rtp over udp:"RTP/AVP", rtp over tcp:"TCP/RTP/AVP". 其他不支持, 填充SDP用 */ boolean inviteAudioBroadcast(String commandFromUserName, String commandFromUserNameAtDomain, String sourceID, String targetID, String addressType, String localAddress, int localPort, String mediaTransportProtocol); /* *取消音频广播, 这个需要在invite收到临时响应之后,最终响应之前才能成功, 如果UAS已经发送过最终响应, UAS收到cancel不做处理, 具体参考RFC3261 */ boolean cancelAudioBroadcast(String sourceID, String targetID); /* *终止语音广播会话, 发送BYE消息 */ boolean byeAudioBroadcast(String sourceID, String targetID); }
RTP音频包接收和解码输出接口,由于我们已经有非常成熟的RTMP和RTSP Player,我们是要在此基础上,扩展一些接口即可:
/* * SmartPlayerJniV2.java * SmartPlayerJniV2 * * Github: https://github.com/daniulive/SmarterStreaming * */ package com.daniulive.smartplayer; public class SmartPlayerJniV2 { /** * Initialize Player(启动播放实例) * * @param ctx: get by this.getApplicationContext() * * <pre>This function must be called firstly.</pre> * * @return player handle if successful, if return 0, which means init failed. */ public native long SmartPlayerOpen(Object ctx); /** * Set External Audio Output(设置回调PCM数据) * * @param handle: return value from SmartPlayerOpen() * * @param external_audio_output: External Audio Output * * @return {0} if successful */ public native int SmartPlayerSetExternalAudioOutput(long handle, Object external_audio_output); /** * Set Audio Data Callback(设置回调编码后音频数据) * * @param handle: return value from SmartPlayerOpen() * * @param audio_data_callback: Audio Data Callback. * * @return {0} if successful */ public native int SmartPlayerSetAudioDataCallback(long handle, Object audio_data_callback); /** * Set buffer(设置缓冲时间,单位:毫秒) * * @param handle: return value from SmartPlayerOpen() * * @param buffer: * * <pre> NOTE: Unit is millisecond, range is 0-5000 ms </pre> * * @return {0} if successful */ public native int SmartPlayerSetBuffer(long handle, int buffer); /** * Set mute or not(设置实时静音) * * @param handle: return value from SmartPlayerOpen() * * @param is_mute: if with 1:mute, if with 0: does not mute * * @return {0} if successful */ public native int SmartPlayerSetMute(long handle, int is_mute); /** * 设置播放音量 * * @param handle: return value from SmartPlayerOpen() * * @param volume: 范围是[0, 100], 0是静音,100是最大音量, 默认是100 * * @return {0} if successful */ public native int SmartPlayerSetAudioVolume(long handle, int volume); /** * 清除所有 rtp receivers * * @param handle: return value from SmartPlayerOpen() * * @return {0} if successful */ public native int SmartPlayerClearRtpReceivers(long handle); /** * 增加 rtp receiver * * @param handle: return value from SmartPlayerOpen() * * @param rtp_receiver_handle: return value from CreateRTPReceiver() * * @return {0} if successful */ public native int SmartPlayerAddRtpReceiver(long handle, long rtp_receiver_handle); /** * 设置需要播放或录像的RTMP/RTSP url * * @param handle: return value from SmartPlayerOpen() * * @param uri: rtsp/rtmp playback/recorder uri * * @return {0} if successful */ public native int SmartPlayerSetUrl(long handle, String uri); /** * Start playback stream(开始播放) * * @param handle: return value from SmartPlayerOpen() * * @return {0} if successful */ public native int SmartPlayerStartPlay(long handle); /** * Stop playback stream(停止播放) * * @param handle: return value from SmartPlayerOpen() * * @return {0} if successful */ public native int SmartPlayerStopPlay(long handle); /** * Start pull stream(开始拉流,用于数据转发,只拉流不播放) * * @param handle: return value from SmartPlayerOpen() * * @return {0} if successful */ public native int SmartPlayerStartPullStream(long handle); /** * Stop pull stream(停止拉流) * * @param handle: return value from SmartPlayerOpen() * * @return {0} if successful */ public native int SmartPlayerStopPullStream(long handle); /** * 关闭播放实例,结束时必须调用close接口释放资源 * * @param handle: return value from SmartPlayerOpen() * * <pre> NOTE: it could not use player handle after call this function. </pre> * * @return {0} if successful */ public native int SmartPlayerClose(long handle); /*++++++++++++++++++RTP Receiver++++++++++++++++++++++*/ /* * 创建RTP Receiver * * @param reserve:保留参数传0 * * @return RTP Receiver 句柄,0表示失败 */ public native long CreateRTPReceiver(int reserve); /** *设置 RTP Receiver传输协议 * * @param rtp_receiver_handle, CreateRTPReceiver * @param transport_protocol, 0:UDP, 1:TCP, 默认是UDP * * @return {0} if successful */ public native int SetRTPReceiverTransportProtocol(long rtp_receiver_handle, int transport_protocol); /** *设置 RTP Receiver IP地址类型 * * @param rtp_receiver_handle, CreateRTPReceiver * @param ip_address_type, 0:IPV4, 1:IPV6, 默认是IPV4 * * @return {0} if successful */ public native int SetRTPReceiverIPAddressType(long rtp_receiver_handle, int ip_address_type); /** *设置 RTP Receiver RTP Socket本地端口 * * @param rtp_receiver_handle, CreateRTPReceiver * @param port, 必须是偶数,设置0的话SDK会自动分配, 默认值是0 * * @return {0} if successful */ public native int SetRTPReceiverLocalPort(long rtp_receiver_handle, int port); /** *设置 RTP Receiver SSRC * * @param rtp_receiver_handle, CreateRTPReceiver * @param ssrc, 如果设置的话,这个字符串要能转换成uint32类型, 否则设置失败 * * @return {0} if successful */ public native int SetRTPReceiverSSRC(long rtp_receiver_handle, String ssrc); /** *创建 RTP Receiver 会话 * * @param rtp_receiver_handle, CreateRTPReceiver * @param reserve, 保留值,目前传0 * * @return {0} if successful */ public native int CreateRTPReceiverSession(long rtp_receiver_handle, int reserve); /** *获取 RTP Receiver RTP Socket本地端口 * * @param rtp_receiver_handle, CreateRTPReceiver * * @return 失败返回0, 成功的话返回响应的端口, 请在CreateRTPReceiverSession返回成功之后调用 */ public native int GetRTPReceiverLocalPort(long rtp_receiver_handle); /** *设置 RTP Receiver Payload 相关信息 * * @param rtp_receiver_handle, CreateRTPReceiver * * @param payload_type, 请参考 RFC 3551 * * @param encoding_name, 编码名, 请参考 RFC 3551, 如果payload_type不是动态的, 可能传null就好 * * @param media_type, 媒体类型, 请参考 RFC 3551, 1 是视频, 2是音频 * * @param clock_rate, 请参考 RFC 3551 * * @return {0} if successful */ public native int SetRTPReceiverPayloadType(long rtp_receiver_handle, int payload_type, String encoding_name, int media_type, int clock_rate); /** *设置 RTP Receiver 音频采样率 * * @param rtp_receiver_handle, CreateRTPReceiver * @param sampling_rate, 音频采样率 * * @return {0} if successful */ public native int SetRTPReceiverAudioSamplingRate(long rtp_receiver_handle, int sampling_rate); /** *设置 RTP Receiver 音频通道数 * * @param rtp_receiver_handle, CreateRTPReceiver * @param channels, 音频通道数 * * @return {0} if successful */ public native int SetRTPReceiverAudioChannels(long rtp_receiver_handle, int channels); /** *设置 RTP Receiver 远端地址 * * @param rtp_receiver_handle, CreateRTPReceiver * @param address, IP地址 * @param port, 端口 * * @return {0} if successful */ public native int SetRTPReceiverRemoteAddress(long rtp_receiver_handle, String address, int port); /** *初始化 RTP Receiver * * @param rtp_receiver_handle, CreateRTPReceiver * * @return {0} if successful */ public native int InitRTPReceiver(long rtp_receiver_handle); /** *UnInit RTP Receiver * * @param rtp_receiver_handle, CreateRTPReceiver * * @return {0} if successful */ public native int UnInitRTPReceiver(long rtp_receiver_handle); /** *Destory RTP Receiver Session * * @param rtp_receiver_handle, CreateRTPReceiver * * @return {0} if successful */ public native int DestoryRTPReceiverSession(long rtp_receiver_handle); /** *Destory RTP Receiver * * @param rtp_receiver_handle, CreateRTPReceiver * * @return {0} if successful */ public native int DestoryRTPReceiver(long rtp_receiver_handle); /*++++++++++++++++++RTP Receiver++++++++++++++++++++++*/ }
上层调用DEMO实例代码:
public class AndroidGB28181Demo implements GBSIPAgentListener { private String gb_source_id_ = null; private String gb_target_id_ = null; private long player_handle_ = 0; private long rtp_receiver_handle_ = 0; private AtomicLong last_receive_audio_data_time_ = new AtomicLong(0); @Override public void ntsOnNotifyBroadcastCommand(String fromUserName, String fromUserNameAtDomain, String sn, String sourceID, String targetID) { handler_.postDelayed(new Runnable() { @Override public void run() { if (gb28181_agent_ != null ) { gb28181_agent_.respondBroadcastCommand(from_user_name_, from_user_name_at_domain_,sn_,source_id_, target_id_, true); } } private String from_user_name_; private String from_user_name_at_domain_; private String sn_; private String source_id_; private String target_id_; public Runnable set(String from_user_name, String from_user_name_at_domain, String sn, String source_id, String target_id) { this.from_user_name_ = from_user_name; this.from_user_name_at_domain_ = from_user_name_at_domain; this.sn_ = sn; this.source_id_ = source_id; this.target_id_ = target_id; return this; } }.set(fromUserName, fromUserNameAtDomain, sn, sourceID, targetID),0); } @Override public void ntsOnAudioBroadcast(String commandFromUserName, String commandFromUserNameAtDomain, String sourceID, String targetID) { handler_.postDelayed(new Runnable() { @Override public void run() { stopAudioPlayer(); destoryRTPReceiver(); if (gb28181_agent_ != null ) { String local_ip_addr = IPAddrUtils.getIpAddress(context_); boolean is_tcp = true; // 默认用TCP rtp_receiver_handle_ = lib_player_.CreateRTPReceiver(0); if (rtp_receiver_handle_ != 0 ) { lib_player_.SetRTPReceiverTransportProtocol(rtp_receiver_handle_, is_tcp?1:0); lib_player_.SetRTPReceiverIPAddressType(rtp_receiver_handle_, 0); if (0 == lib_player_.CreateRTPReceiverSession(rtp_receiver_handle_, 0) ) { int local_port = lib_player_.GetRTPReceiverLocalPort(rtp_receiver_handle_); boolean ret = gb28181_agent_.inviteAudioBroadcast(command_from_user_name_,command_from_user_name_at_domain_, source_id_, target_id_, "IP4", local_ip_addr, local_port, is_tcp?"TCP/RTP/AVP":"RTP/AVP"); if (!ret ) { destoryRTPReceiver(); } } else { destoryRTPReceiver(); } } } } private String command_from_user_name_; private String command_from_user_name_at_domain_; private String source_id_; private String target_id_; public Runnable set(String command_from_user_name, String command_from_user_name_at_domain, String source_id, String target_id) { this.command_from_user_name_ = command_from_user_name; this.command_from_user_name_at_domain_ = command_from_user_name_at_domain; this.source_id_ = source_id; this.target_id_ = target_id; return this; } }.set(commandFromUserName, commandFromUserNameAtDomain, sourceID, targetID),0); } @Override public void ntsOnInviteAudioBroadcastException(String sourceID, String targetID, String errorInfo) { handler_.postDelayed(new Runnable() { @Override public void run() { destoryRTPReceiver(); } private String source_id_; private String target_id_; public Runnable set(String source_id, String target_id) { this.source_id_ = source_id; this.target_id_ = target_id; return this; } }.set(sourceID, targetID),0); } @Override public void ntsOnInviteAudioBroadcastTimeout(String sourceID, String targetID) { handler_.postDelayed(new Runnable() { @Override public void run() { destoryRTPReceiver(); } private String source_id_; private String target_id_; public Runnable set(String source_id, String target_id) { this.source_id_ = source_id; this.target_id_ = target_id; return this; } }.set(sourceID, targetID),0); } class PlayerExternalPCMOutput implements NTExternalAudioOutput { private int buffer_size_ = 0; private ByteBuffer pcm_buffer_ = null; @Override public ByteBuffer getPcmByteBuffer(int size) { if(size < 1) return null; if(buffer_size_ != size) { buffer_size_ = size; pcm_buffer_ = ByteBuffer.allocateDirect(buffer_size_); } return pcm_buffer_; } public void onGetPcmFrame(int ret, int sampleRate, int channel, int sampleSize, int is_low_latency) { if (null == pcm_buffer_) return; pcm_buffer_.rewind(); if (ret == 0 && isGB28181StreamRunning && publisherHandle != 0 ) // 传给发送端做音频相关处理 libPublisher.SmartPublisherOnFarEndPCMData(publisherHandle, pcm_buffer_, sampleRate, channel, sampleSize, is_low_latency); } } class PlayerAudioDataOutput implements NTAudioDataCallback { private int buffer_size_ = 0; private int param_info_size_ = 0; private ByteBuffer buffer_ = null; private ByteBuffer parameter_info_ = null; @Override public ByteBuffer getAudioByteBuffer(int size) { if( size < 1 ) return null; if (size <= buffer_size_ && buffer_ != null ) return buffer_; buffer_size_ = align(size + 256, 16); buffer_ = ByteBuffer.allocateDirect(buffer_size_); return buffer_; } @Override public ByteBuffer getAudioParameterInfo(int size) { if(size < 1) return null; if ( size <= param_info_size_ && parameter_info_ != null ) return parameter_info_; param_info_size_ = align(size + 32, 16); parameter_info_ = ByteBuffer.allocateDirect(param_info_size_); return parameter_info_; } public void onAudioDataCallback(int ret, int audio_codec_id, int sample_size, int is_key_frame, long timestamp, int sample_rate, int channel, int parameter_info_size, long reserve) { last_receive_audio_data_time_.set(SystemClock.elapsedRealtime()); } } class AudioPlayerDataTimer implements Runnable { public static final int THRESHOLD_MS = 60*1000; public static final int INTERVAL_MS = 10*1000; public AudioPlayerDataTimer(long handle) { handle_ = handle; } @Override public void run() { if (0 == handle_) return; if (handle_ != player_handle_) return; long last_update_time = last_receive_audio_data_time_.get(); long cur_time = SystemClock.elapsedRealtime(); if ( (last_update_time + this.THRESHOLD_MS) > cur_time) { // 继续定时器 handler_.postDelayed(new AudioPlayerDataTimer(this.handle_), this.INTERVAL_MS); } else { if (gb_source_id_!= null && gb_target_id_ != null) { if (gb28181_agent_ != null) gb28181_agent_.byeAudioBroadcast(gb_source_id_, gb_target_id_); } gb_source_id_= null; gb_target_id_ = null; stopAudioPlayer(); destoryRTPReceiver(); } } private long handle_; } private boolean startAudioPlay() { if (player_handle_ != 0 ) return false; player_handle_ = lib_player_.SmartPlayerOpen(context_); if (player_handle_ == 0) return false; // lib_player_.SetSmartPlayerEventCallbackV2(player_handle_,new EventHandePlayerV2()); lib_player_.SmartPlayerSetBuffer(player_handle_, 0); lib_player_.SmartPlayerSetReportDownloadSpeed(player_handle_, 1, 10); lib_player_.SmartPlayerClearRtpReceivers(player_handle_); lib_player_.SmartPlayerAddRtpReceiver(player_handle_, rtp_receiver_handle_); lib_player_.SmartPlayerSetSurface(player_handle_, null); // lib_player_.SmartPlayerSetRenderScaleMode(player_handle_, 1); lib_player_.SmartPlayerSetAudioOutputType(player_handle_, 1); lib_player_.SmartPlayerSetMute(player_handle_, 0); lib_player_.SmartPlayerSetAudioVolume(player_handle_, 100); lib_player_.SmartPlayerSetExternalAudioOutput(player_handle_, new PlayerExternalPCMOutput()); lib_player_.SmartPlayerSetUrl(player_handle_, "rtp://xxxxxxxxxxxxxxxxxxx"); if (0 != lib_player_.SmartPlayerStartPlay(player_handle_)) { lib_player_.SmartPlayerClose(player_handle_); player_handle_ = 0; Log.e(TAG, "start audio paly failed"); return false; } lib_player_.SmartPlayerSetAudioDataCallback(player_handle_, new PlayerAudioDataOutput()); if (0 ==lib_player_.SmartPlayerStartPullStream(player_handle_) ) { // 启动定时器,长时间收不到音频数据,则停止播放,发送BYE last_receive_audio_data_time_.set(SystemClock.elapsedRealtime()); handler_.postDelayed(new AudioPlayerDataTimer(player_handle_), AudioPlayerDataTimer.INTERVAL_MS); } return true; } private void stopAudioPlayer() { if (player_handle_ != 0 ) { lib_player_.SmartPlayerStopPullStream(player_handle_); lib_player_.SmartPlayerStopPlay(player_handle_); lib_player_.SmartPlayerClose(player_handle_); player_handle_ = 0; } } private void destoryRTPReceiver() { if (rtp_receiver_handle_ != 0) { lib_player_.UnInitRTPReceiver(rtp_receiver_handle_); lib_player_.DestoryRTPReceiverSession(rtp_receiver_handle_); lib_player_.DestoryRTPReceiver(rtp_receiver_handle_); rtp_receiver_handle_ = 0; } } @Override public void ntsOnInviteAudioBroadcastResponse(String sourceID, String targetID, int statusCode, PlaySessionDescription sessionDescription) { handler_.postDelayed(new Runnable() { @Override public void run() { boolean is_need_destory_rtp = true; if (gb28181_agent_ != null ) { boolean is_need_bye = 200==status_code_; if (200 == status_code_ && session_description_ != null && rtp_receiver_handle_ != 0 ) { MediaSessionDescription audio_des = session_description_.getAudioDescription(); SDPRtpMapAttribute audio_attr = null; if (audio_des != null && audio_des.getRtpMapAttributes() != null && !audio_des.getRtpMapAttributes().isEmpty() ) audio_attr = audio_des.getRtpMapAttributes().get(0); if ( audio_des != null && audio_attr != null ) { lib_player_.SetRTPReceiverSSRC(rtp_receiver_handle_, audio_des.getSSRC()); int clock_rate = audio_attr.getClockRate(); lib_player_.SetRTPReceiverPayloadType(rtp_receiver_handle_, audio_attr.getPayloadType(), audio_attr.getEncodingName(), 2, clock_rate); // 如果是PCMA, 会默认填采样率8000, 通道1, 其他音频编码需要手动填入 // lib_player_.SetRTPReceiverAudioSamplingRate(rtp_receiver_handle_, 8000); // lib_player_.SetRTPReceiverAudioChannels(rtp_receiver_handle_, 1); lib_player_.SetRTPReceiverRemoteAddress(rtp_receiver_handle_, audio_des.getAddress(), audio_des.getPort()); lib_player_.InitRTPReceiver(rtp_receiver_handle_); if (startAudioPlay()) { is_need_bye = false; is_need_destory_rtp = false; gb_source_id_ = source_id_; gb_target_id_ = target_id_; } } } if (is_need_bye) gb28181_agent_.byeAudioBroadcast(source_id_, target_id_); } if (is_need_destory_rtp) destoryRTPReceiver(); } private String source_id_; private String target_id_; private int status_code_; private PlaySessionDescription session_description_; public Runnable set(String source_id, String target_id, int status_code, PlaySessionDescription session_description) { this.source_id_ = source_id; this.target_id_ = target_id; this.status_code_ = status_code; this.session_description_ = session_description; return this; } }.set(sourceID, targetID, statusCode, sessionDescription),0); } @Override public void ntsOnByeAudioBroadcast(String sourceID, String targetID) { handler_.postDelayed(new Runnable() { @Override public void run() { gb_source_id_ = null; gb_target_id_ = null; stopAudioPlayer(); destoryRTPReceiver(); } private String source_id_; private String target_id_; public Runnable set(String source_id, String target_id) { this.source_id_ = source_id; this.target_id_ = target_id; return this; } }.set(sourceID, targetID),0); } @Override public void ntsOnTerminateAudioBroadcast(String sourceID, String targetID) { handler_.postDelayed(new Runnable() { @Override public void run() { gb_source_id_ = null; gb_target_id_ = null; stopAudioPlayer(); destoryRTPReceiver(); } private String source_id_; private String target_id_; public Runnable set(String source_id, String target_id) { this.source_id_ = source_id; this.target_id_ = target_id; return this; } }.set(sourceID, targetID),0); } }
以上是大概的流程,感兴趣的开发者,可Q我89030985,通过自测和现场的反馈,由于我们有回音消除机制,整体的体验还是非常不错的。
有开发者私信我们,如果从头开发Android平台的GB28181接入端,需要多久?我想说的是,如果是按照SPEC实现个DEMO,验证技术可行性的话不难,但是如果是产品级,确保功能完备性能优异长时间运行稳定的话,从头开发,难度还是挺大的。