上篇文章提到Android端GB28181接入端的語音廣播和語音對講的實作,從spec角度大概介紹了下流程和簡單的接口設計,好多開發者私信我,希望展開說一下。其實這塊難度不大,隻是廣播和對講涉及到雙向實作,如果之前沒有相關的積累,從頭實作麻煩一些而已。
語音廣播的流程大家應該非常清楚了,簡單來說,SIP伺服器發送Broadcast語音廣播指令到android接入端,接入端應答,在收到200 OK後,發送INVITE消息,Android接入端收到INVITE的200 OK響應後,回複ACK,開始讀取并解析RTP包,然後對音頻資料解碼,輸出到Android播放裝置即可。
從DEMO來看,當有語音廣播接入進來後,GB28181語音廣播按鈕會處于可用狀态。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0gTMx81dsQWZ4lmZf1GLlpXazVmcvwVZnFWbp1zczV2YvJHctM3cv1Ces0zaHRGcWdUYuVzVa9GczoVdG1mWfVGc5RHLwIzX39GZhh2csATMflHLwEzX4xSZz91ZsAzMfRHLGZkRGZkRfJ3bs92YskmNhVTYykVNQJVMRhXVEF1X0hXZ0xCNx8VZ6l2cssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnLlhDZjFDNwATM1MGM1cjMlVjNxMWNjNGO2UTZzQzN0I2MmBTN3YzLchDMyIDMy8CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
語音廣播信令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;
/*
* 音頻廣播, 收到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;
/*
*語音廣播接收者發送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;
/*
*取消音頻廣播, 這個需要在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
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
public native int SmartPlayerSetExternalAudioOutput(long;
/**
* Set Audio Data Callback(設定回調編碼後音頻資料)
*
* @param handle: return value from SmartPlayerOpen()
*
* @param audio_data_callback: Audio Data Callback.
*
* @return
public native int SmartPlayerSetAudioDataCallback(long;
/**
* Set buffer(設定緩沖時間,機關:毫秒)
*
* @param handle: return value from SmartPlayerOpen()
*
* @param buffer:
*
* <pre> NOTE: Unit is millisecond, range is 0-5000 ms </pre>
*
* @return
public native int SmartPlayerSetBuffer(long handle, int;
/**
* Set mute or not(設定實時靜音)
*
* @param handle: return value from SmartPlayerOpen()
*
* @param is_mute: if with 1:mute, if with 0: does not mute
*
* @return
public native int SmartPlayerSetMute(long handle, int;
/**
* 設定播放音量
*
* @param handle: return value from SmartPlayerOpen()
*
* @param volume: 範圍是[0, 100], 0是靜音,100是最大音量, 預設是100
*
* @return
public native int SmartPlayerSetAudioVolume(long handle, int;
/**
* 清除所有 rtp receivers
*
* @param handle: return value from SmartPlayerOpen()
*
* @return
public native int SmartPlayerClearRtpReceivers(long;
/**
* 增加 rtp receiver
*
* @param handle: return value from SmartPlayerOpen()
*
* @param rtp_receiver_handle: return value from CreateRTPReceiver()
*
* @return
public native int SmartPlayerAddRtpReceiver(long handle, long;
/**
* 設定需要播放或錄像的RTMP/RTSP url
*
* @param handle: return value from SmartPlayerOpen()
*
* @param uri: rtsp/rtmp playback/recorder uri
*
* @return
public native int SmartPlayerSetUrl(long;
/**
* Start playback stream(開始播放)
*
* @param handle: return value from SmartPlayerOpen()
*
* @return
public native int SmartPlayerStartPlay(long;
/**
* Stop playback stream(停止播放)
*
* @param handle: return value from SmartPlayerOpen()
*
* @return
public native int SmartPlayerStopPlay(long;
/**
* Start pull stream(開始拉流,用于資料轉發,隻拉流不播放)
*
* @param handle: return value from SmartPlayerOpen()
*
* @return
public native int SmartPlayerStartPullStream(long;
/**
* Stop pull stream(停止拉流)
*
* @param handle: return value from SmartPlayerOpen()
*
* @return
public native int SmartPlayerStopPullStream(long;
/**
* 關閉播放執行個體,結束時必須調用close接口釋放資源
*
* @param handle: return value from SmartPlayerOpen()
*
* <pre> NOTE: it could not use player handle after call this function. </pre>
*
* @return
public native int SmartPlayerClose(long;
/*++++++++++++++++++RTP Receiver++++++++++++++++++++++*/
/*
* 建立RTP Receiver
*
* @param reserve:保留參數傳0
*
* @return RTP Receiver 句柄,0表示失敗
*/
public native long CreateRTPReceiver(int;
/**
*設定 RTP Receiver傳輸協定
*
* @param rtp_receiver_handle, CreateRTPReceiver
* @param transport_protocol, 0:UDP, 1:TCP, 預設是UDP
*
* @return
public native int SetRTPReceiverTransportProtocol(long rtp_receiver_handle, int;
/**
*設定 RTP Receiver IP位址類型
*
* @param rtp_receiver_handle, CreateRTPReceiver
* @param ip_address_type, 0:IPV4, 1:IPV6, 預設是IPV4
*
* @return
public native int SetRTPReceiverIPAddressType(long rtp_receiver_handle, int;
/**
*設定 RTP Receiver RTP Socket本地端口
*
* @param rtp_receiver_handle, CreateRTPReceiver
* @param port, 必須是偶數,設定0的話SDK會自動配置設定, 預設值是0
*
* @return
public native int SetRTPReceiverLocalPort(long rtp_receiver_handle, int;
/**
*設定 RTP Receiver SSRC
*
* @param rtp_receiver_handle, CreateRTPReceiver
* @param ssrc, 如果設定的話,這個字元串要能轉換成uint32類型, 否則設定失敗
*
* @return
public native int SetRTPReceiverSSRC(long;
/**
*建立 RTP Receiver 會話
*
* @param rtp_receiver_handle, CreateRTPReceiver
* @param reserve, 保留值,目前傳0
*
* @return
public native int CreateRTPReceiverSession(long rtp_receiver_handle, int;
/**
*擷取 RTP Receiver RTP Socket本地端口
*
* @param rtp_receiver_handle, CreateRTPReceiver
*
* @return
public native int GetRTPReceiverLocalPort(long;
/**
*設定 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
public native int SetRTPReceiverPayloadType(long rtp_receiver_handle, int payload_type, String encoding_name, int media_type, int;
/**
*設定 RTP Receiver 音頻采樣率
*
* @param rtp_receiver_handle, CreateRTPReceiver
* @param sampling_rate, 音頻采樣率
*
* @return
public native int SetRTPReceiverAudioSamplingRate(long rtp_receiver_handle, int;
/**
*設定 RTP Receiver 音頻通道數
*
* @param rtp_receiver_handle, CreateRTPReceiver
* @param channels, 音頻通道數
*
* @return
public native int SetRTPReceiverAudioChannels(long rtp_receiver_handle, int;
/**
*設定 RTP Receiver 遠端位址
*
* @param rtp_receiver_handle, CreateRTPReceiver
* @param address, IP位址
* @param port, 端口
*
* @return
public native int SetRTPReceiverRemoteAddress(long rtp_receiver_handle, String address, int;
/**
*初始化 RTP Receiver
*
* @param rtp_receiver_handle, CreateRTPReceiver
*
* @return
public native int InitRTPReceiver(long;
/**
*UnInit RTP Receiver
*
* @param rtp_receiver_handle, CreateRTPReceiver
*
* @return
public native int UnInitRTPReceiver(long;
/**
*Destory RTP Receiver Session
*
* @param rtp_receiver_handle, CreateRTPReceiver
*
* @return
public native int DestoryRTPReceiverSession(long;
/**
*Destory RTP Receiver
*
* @param rtp_receiver_handle, CreateRTPReceiver
*
* @return
public native int DestoryRTPReceiver(long;
/*++++++++++++++++++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 {
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 {
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 {
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 {
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 {
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;
}
@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 {
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 {
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);
}
}