技术背景
语音广播功能是GB28181设备接入端非常重要的功能属性,语音广播让终端和平台之间,有了实时双向互动,可以满足执法记录仪、智能安全帽、智能监控、智慧零售、智慧教育、远程办公、明厨亮灶、智慧交通、智慧工地、雪亮工程、平安乡村、生产运输、车载终端等场景的技术诉求。
这里我们先回顾下GB28181规范关于语音广播的描述:
语音广播功能实现用户通过语音输入设备向前端语音输出设备的语音广播。
语音输入设备/语音输入联网系统(以下简称“语音流发送者”)、SIP 服务器向语音输出设备/语音输出视频监控联网系统(以下简称“语音流接收者”)发送通知消息,语音流接收者收到通知消息后,进行判断处理。
若能够接收广播,则向语音流发送者发起呼叫请求,获取广播媒体流。
语音输入设备、语音输出设备编码应符合E.1 的规定。如果设备具备语音输出能力,则在设备目录查询和订阅时,需要上报语音输出设备。如果不上报语音输出设备,则表示该设备没有语音输出能力。
上报语音输出通道时,ParentID 填写其父设备的 ID。例如,IPC 具备语音输出能力,在 IPC 上报设备目录时,需要上报语音输出设备。该语音输出设备ID的类型编码为 137,其父设备为该IPC。NVR 本身具备语音输出能力,在 NVR 上报设备目录时,除了上报 NVR 接入的 IPC 以及IPC 自身的语音输出设备之外,还需要上报语音输出设备。该语音输出设备ID的类型编码为137.其父设备为该NVR。监控中心与设备之间进行语音广播,可以直接对语音输出设备发送语音广播通知,也可以对语音输出设备所属的前端主设备发送语音广播通知。
对前端主设备发送语音广播通知消息中仅需携带前端主设备编码,表示对该设备上所有的语音输出设备进行语音广播。例如,对IPC 发送语音广播通知,表示对该IPC 接入的所有语音输出设备进行广播;对 NVR 发送语音广播通知,表示对 NVR 下所有 IPC以及自身的语音输出设备进行广播。
语音流的封装格式应符合 C.2.4 音频流的 RTP 封装的定义。
语音广播宜符合附录 K 规定的媒体流保活机制。
技术实现
本文我们不再探讨GB28181语音广播的具体流程,这里我们假定信令交互已经完成,准备接收数据:
收到broadcast语音广播后,我们的处理逻辑如下:
代码语言:javascript复制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 EventHandlerPlayerV2());
// 缓存大小可以调整
lib_player_.SmartPlayerSetBuffer(player_handle_, 0);
// lib_player_.SmartPlayerSetFastStartup(player_handle_, 0);
// set report download speed(默认2秒一次回调 用户可自行调整report间隔)
lib_player_.SmartPlayerSetReportDownloadSpeed(player_handle_, 1, 2);
lib_player_.SmartPlayerClearRtpReceivers(player_handle_);
lib_player_.SmartPlayerAddRtpReceiver(player_handle_, rtp_receiver_handle_);
lib_player_.SmartPlayerSetSurface(player_handle_, null);
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://ntinternal/rtpreceiver/implemention0");
if (0 != lib_player_.SmartPlayerStartPlay(player_handle_)) {
lib_player_.SmartPlayerClose(player_handle_);
player_handle_ = 0;
Log.e(TAG, "[daniusdk]start audio play failed");
return false;
}
lib_player_.SmartPlayerSetAudioDataCallback(player_handle_, new PlayerAudioDataOutput());
lib_player_.SmartPlayerSetPullStreamAudioTranscodeAAC(player_handle_, 0);
if (0 ==lib_player_.SmartPlayerStartPullStream(player_handle_) ) {
// 启动定时器,长时间收不到音频数据,则停止播放,发送BYE
last_received_audio_data_time_.set(SystemClock.elapsedRealtime());
handler_.postDelayed(new AudioPlayerPCMTimer(player_handle_), AudioPlayerPCMTimer.INTERVAL_MS);
}
return true;
}
简单来说,就是启动了个纯语音播放的实例,来处理过来的PCMA或PS的audio数据。
其中PlayerExternalPCMOutput()主要是把数据塞到GB28181数据采集处理的模块,来实现语音广播的回音消除的目的。
代码语言:javascript复制class PlayerExternalPCMOutput implements NTExternalAudioOutput {
private int buffer_size_ = 0;
private ByteBuffer pcm_buffer_ = null;
@Override
public ByteBuffer getPcmByteBuffer(int size) {
//Log.i("getPcmByteBuffer", "size: " 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);
}
}
如果需要停止播放,调用以下逻辑即可:
代码语言:javascript复制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;
}
}
总结
GB28181语音广播这块,如果平台侧和终端,都是按照规范来实现的话,问题会少很多,实际尴尬的是,大厂或部分厂商先入为主,实际生产环境,不一定按照预期的,谁的问题谁处理,作为Android终端模块,push不动国标平台侧的时候,有时候只有兼容它,这种痛苦真是一言难尽。