欢迎光临
我们一直在努力

Android平台GB28181接入实战:基于SmartGBD实现设备注册、实时视频推送与语音广播

前言

GB/T 28181是我国公共安全行业的强制性标准,全称《公共安全视频监控联网系统信息传输、交换、控制技术要求》,当前主流版本为GB/T 28181-2016与GB/T 28181-2022。该标准规定了视频监控联网系统中SIP信令的交互流程、媒体流的传输格式,以及设备管理、云台控制、语音广播等业务功能的实现方式。

在移动安防、执法记录仪、智慧工地、车载监控等越来越多的场景下,Android设备需要作为GB28181前端设备接入平台级的监控中心(如EHome、宇视、海康ISC等)。本文将结合大牛直播SDK(SmartGBD)的Android Demo代码,系统梳理Android端GB28181接入的完整实现方案,内容涵盖:

  • GB28181核心信令流程梳理
  • SIP代理初始化、注册与心跳维持
  • 实时视音频采集与编码
  • 媒体流推送(RTP/PS封装)
  • 语音广播(双向对讲)
  • 云台控制指令响应
  • 录像查询与回放下载
  • 截图上传(快照配置)

一、GB28181协议关键流程概述

1.1 设备注册与心跳

GB28181使用SIP协议(RFC 3261)作为信令承载。前端设备(IPC/NVR/移动终端)向SIP服务器发送REGISTER请求完成注册,注册有效期(Expires)默认不低于3600秒。注册成功后,设备需定期发送MESSAGE请求维持心跳,心跳间隔通常为20~60秒,连续心跳失败次数达到阈值则认为设备离线并触发重连。

设备 —REGISTER—> SIP服务器
设备 <–401 Unauthorized— SIP服务器(含WWW-Authenticate)
设备 —REGISTER(含Authorization)—> SIP服务器
设备 <–200 OK— SIP服务器
设备 —MESSAGE(Keepalive)—> SIP服务器(每隔heartbeat_interval秒)
SIP服务器 <–200 OK— 设备

1.2 实时视频邀请流程(INVITE)

当监控平台需要查看前端设备画面时,向设备发送INVITE请求,SDP中携带目标RTP端口、SSRC及媒体格式描述。设备解析SDP并创建RTP Sender,在返回200 OK中填写本端RTP信息,平台回ACK后开始推流。

SIP服务器 —INVITE(SDP:目标IP/Port/SSRC)—> 设备
设备 <—180 Ringing— (振铃响应)
设备 <—200 OK(SDP:本端IP/Port)— 设备
SIP服务器 —ACK—> 设备
(开始RTP/PS推流)
SIP服务器 —BYE—> 设备(停止推流)

GB28181规定媒体流采用PS(Program Stream)封装,外层包裹在RTP中传输,Payload Type通常为96,编码名为"PS",时钟频率为90000Hz。

1.3 语音广播流程

平台向设备发送广播通知(MESSAGE,携带Broadcast XML),设备响应并回复确认后,平台再INVITE设备,设备创建RTP Receiver接收平台推送的音频流(PCMA/PCMU/AAC),并将接收到的音频数据通过扬声器播放或混入推流。

1.4 录像查询与回放

  • 录像查询:平台通过MESSAGE发送录像查询命令(RecordInfo),设备查询本地录像文件列表后回复。
  • 历史回放:平台INVITE,SDP中type字段为playback(on-demand流),设备以正常2倍速或实际速率发送历史视频数据。
  • 录像下载:与回放类似,通常以4倍速或更快速率传输,SDP中type字段为download。

二、SDK初始化与权限配置

2.1 AndroidManifest权限声明

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

2.2 加载Native库

static {
System.loadLibrary("SmartPublisher");
System.loadLibrary("SmartPlayer");
}

SDK采用Publisher(推流/编码)和Player(拉流/解码)两个模块,GB28181推流基于Publisher,语音广播接收基于Player。

2.3 SDK授权与RTSP服务初始化

// 如需设置授权Key, 请确保在SmartPublisherOpen之前调用
// libPublisher.SmartPublisherSetSDKClientKey("your_cid", "your_key", 0);
// lib_player_.SmartPlayerSetSDKClientKey("your_cid", "your_key", 0);

libPublisher = new SmartPublisherJniV2();
lib_player_ = new SmartPlayerJniV2();


三、摄像头采集配置

3.1 Camera2Helper封装

Demo使用Camera2 API采集,通过Camera2Helper封装了开/关/切换摄像头,以及分辨率切换逻辑。

void initCamera() {
camera2Helper = new Camera2Helper.Builder()
.cameraListener(this) // 实现Camera2Listener接口
.maxPreviewSize(new Point(1920, 1080))
.minPreviewSize(new Point(1280, 720))
.specificCameraId(CAMERA_ID) // CAMERA_ID_BACK / CAMERA_ID_FRONT
.context(this)
.previewOn(textureView)
.isMirror(is_mirror)
.previewViewSize(new Point(textureView.getWidth(), textureView.getHeight()))
.screenOrientation(screen_orientation_)
.scaleToFit(Matrix.ScaleToFit.CENTER)
.build();

camera2Helper.start(video_width_, video_height_);
}

支持的分辨率档位:

档位分辨率
0 1280×720(默认)
1 640×480
2 1920×1080

3.2 图像帧投递(YUV420888)

Camera2采集的图像格式为YUV_420_888,通过PostLayerImageYUV420888ByteBuffer投递给SDK的第0层(基础视频层):

@Override
public void onCameraImageData(Image image) {
Image.Plane[] planes = image.getPlanes();

int w = image.getWidth(), h = image.getHeight();
int y_offset = 0, u_offset = 0, v_offset = 0;

Rect crop_rect = image.getCropRect();
if (crop_rect != null && !crop_rect.isEmpty()) {
w = crop_rect.width();
h = crop_rect.height();
y_offset += crop_rect.top * planes[0].getRowStride() + crop_rect.left * planes[0].getPixelStride();
u_offset += (crop_rect.top / 2) * planes[1].getRowStride() + (crop_rect.left / 2) * planes[1].getPixelStride();
v_offset += (crop_rect.top / 2) * planes[2].getRowStride() + (crop_rect.left / 2) * planes[2].getPixelStride();
}

int rotation_degree = cameraImageRotationDegree_;
if (rotation_degree < 0) {
return; // 方向尚未确定,跳过
}

int scale_filter_mode = 3; // 高质量缩放

// 投递给stream_publisher_和snap_shot_publisher_两个实例
for (LibPublisherWrapper i : publisher_array_)
i.PostLayerImageYUV420888ByteBuffer(0, 0, 0,
planes[0].getBuffer(), y_offset, planes[0].getRowStride(),
planes[1].getBuffer(), u_offset, planes[1].getRowStride(),
planes[2].getBuffer(), v_offset, planes[2].getRowStride(),
planes[1].getPixelStride(),
w, h, 0, 0,
scale_w, scale_h, scale_filter_mode, rotation_degree);
}

重点说明:rotation_degree来源于陀螺仪方向传感器与摄像头特性的组合计算,确保无论设备横竖屏、前后摄均能输出方向正确的视频画面,这对GB28181监控场景尤为重要。


四、Publisher初始化与编码参数配置

4.1 打开Publisher实例

private void InitAndSetConfig() {
if (null == libPublisher || !stream_publisher_.empty())
return;

int audio_opt = 1; // 1: 推送编码前音频(PCM)
int video_opt = 3; // 3: 层叠加模式(Layer模式,支持水印叠加)

long handle = libPublisher.SmartPublisherOpen(context_, audio_opt, video_opt,
video_width_, video_height_);
if (0 == handle) {
Log.e(TAG, "sdk open failed!");
return;
}

int fps = 25;
int gop = fps * 3; // 3秒一个GOP

initialize_publisher(libPublisher, handle, video_width_, video_height_, fps, gop);

stream_publisher_.set(libPublisher, handle);
}

4.2 视频编码器选择

SDK支持H.264软编、H.264硬编、H.265硬编三种模式:

private boolean initialize_publisher(SmartPublisherJniV2 lib_publisher, long handle,
int width, int height, int fps, int gop) {

if (videoEncodeType == 1) {
// H.264硬编码
int kbps = LibPublisherWrapper.estimate_video_hardware_kbps(width, height, fps, true);
int isSupportH264HWEncoder = lib_publisher.SetSmartPublisherVideoHWEncoder(handle, kbps);
if (isSupportH264HWEncoder == 0) {
lib_publisher.SetNativeMediaNDK(handle, 0);
lib_publisher.SetVideoHWEncoderBitrateMode(handle, 1); // VBR模式
lib_publisher.SetVideoHWEncoderQuality(handle, 39);
lib_publisher.SetAVCHWEncoderProfile(handle, 0x08); // High Profile
lib_publisher.SetAVCHWEncoderLevel(handle, 0x1000); // Level 4.1
}
} else if (videoEncodeType == 2) {
// H.265硬编码
int kbps = LibPublisherWrapper.estimate_video_hardware_kbps(width, height, fps, false);
int isSupportHevcHWEncoder = lib_publisher.SetSmartPublisherVideoHevcHWEncoder(handle, kbps);
if (isSupportHevcHWEncoder == 0) {
lib_publisher.SetNativeMediaNDK(handle, 0);
lib_publisher.SetVideoHWEncoderBitrateMode(handle, 1);
lib_publisher.SetVideoHWEncoderQuality(handle, 39);
}
}

// 软编VBR模式(适用于软编H.264)
boolean is_sw_vbr_mode = true;
if (is_sw_vbr_mode) {
int video_quality = LibPublisherWrapper.estimate_video_software_quality(width, height, true);
int vbr_max_kbps = LibPublisherWrapper.estimate_video_vbr_max_kbps(width, height, fps);
lib_publisher.SmartPublisherSetSwVBRMode(handle, 1, video_quality, vbr_max_kbps);
}

// 音频编码类型:PCMA(3) 或 AAC(1)
if (is_pcma_) {
lib_publisher.SmartPublisherSetAudioCodecType(handle, 3); // PCMA,8kHz适合GB28181
} else {
lib_publisher.SmartPublisherSetAudioCodecType(handle, 1); // AAC
}

// 事件回调
lib_publisher.SetSmartPublisherEventCallbackV2(handle,
new EventHandlerPublisherV2().set(handler_, record_executor_));

// H.264编码Profile与速度
lib_publisher.SmartPublisherSetSWVideoEncoderProfile(handle, 3); // High Profile
lib_publisher.SmartPublisherSetSWVideoEncoderSpeed(handle, 2);
lib_publisher.SmartPublisherSetGopInterval(handle, gop);
lib_publisher.SmartPublisherSetFPS(handle, fps);

// 音频处理
lib_publisher.SmartPublisherSetNoiseSuppression(handle, 1); // 噪音抑制
lib_publisher.SmartPublisherSetAGC(handle, 0); // 关闭AGC
lib_publisher.SmartPublisherSetEchoCancellation(handle, 1, 0); // 回声消除

return true;
}

码率估算辅助方法:SDK提供了estimate_video_hardware_kbps、estimate_video_software_quality、estimate_video_vbr_max_kbps等静态方法,根据分辨率和帧率给出推荐的编码参数,便于开发者快速适配不同场景。


五、GB28181 SIP代理初始化

GB28181配置参数支持从SharedPreferences持久化存储读取,通过GB28181ConfigActivity进行可视化配置:

5.1 参数定义

private int gb28181_sip_local_port_base_ = 5060;
private String gb28181_sip_server_id_ = "34020000002000000001";
private String gb28181_sip_domain_ = "3402000000";
private String gb28181_sip_server_addr_ = "192.168.0.105";
private int gb28181_sip_server_port_ = 15060;
private String gb28181_sip_username_ = "34020000011310000039";
private String gb28181_sip_password_ = "12345678";
private String gb28181_channel_id_ = "34020000001380000001"; // 通道ID
private int gb28181_reg_expired_ = 3600;
private int gb28181_heartbeat_interval_ = 20;
private int gb28181_heartbeat_count_ = 3;
private int gb28181_sip_trans_protocol_ = 0; // 0:UDP, 1:TCP

ID编码规则说明(依据GB/T 28181附录D):国标ID为20位数字,格式为:

  • 前8位:行政区划编码(如340200表示安徽省芜湖市)
  • 第9~10位:SIP接入类型(00=平台,13=摄像头,其他见标准)
  • 后10位:设备序号(自行分配,保证唯一性)

5.2 加载配置

private void loadGB28181Config() {
SharedPreferences sp = getSharedPreferences("GB28181Config", MODE_PRIVATE);
gb28181_sip_local_port_base_ = sp.getInt("sip_local_port", 5060);
gb28181_sip_server_id_ = sp.getString("sip_server_id", "34020000002000000001");
gb28181_sip_domain_ = sp.getString("sip_domain", "3402000000");
gb28181_sip_server_addr_= sp.getString("sip_server_addr", "192.168.0.105");
gb28181_sip_server_port_= sp.getInt("sip_server_port", 15060);
gb28181_sip_user_agent_filed_ = sp.getString("user_agent", "");
gb28181_sip_username_ = sp.getString("username", "34020000011310000039");
gb28181_sip_password_ = sp.getString("password", "12345678");
gb28181_reg_expired_ = sp.getInt("reg_expired", 3600);
gb28181_heartbeat_interval_ = sp.getInt("heartbeat_interval", 20);
gb28181_heartbeat_count_ = sp.getInt("heartbeat_count", 3);
gb28181_sip_trans_protocol_ = sp.getInt("trans_protocol", 0);
gb28181_channel_id_ = sp.getString("device_id", "34020000001380000001");
}

5.3 SIP代理创建与启动

private boolean initGB28181Agent() {
if (gb28181_agent_ != null) return true;

getLocation(context_); // 获取GPS位置(用于MobilePosition上报)
loadGB28181Config();

String local_ip_addr = IPAddrUtils.getIpAddress(context_);
if (local_ip_addr == null || local_ip_addr.isEmpty()) {
Log.e(TAG, "initGB28181Agent local ip is empty");
return false;
}

gb28181_agent_ = GBSIPAgentFactory.getInstance().create();
if (gb28181_agent_ == null) return false;

// 注册各类事件监听器
gb28181_agent_.addListener(this); // 注册/心跳事件
gb28181_agent_.addPlayListener(this); // 实时视频INVITE事件
gb28181_agent_.addAudioBroadcastListener(this); // 语音广播事件
gb28181_agent_.addDeviceControlListener(this); // 设备控制(PTZ等)
gb28181_agent_.setDeviceConfigListener(this); // 设备配置(快照等)
gb28181_agent_.addQueryCommandListener(this); // 预置位查询
gb28181_agent_.addQueryRecordInfoListener(this);// 录像查询

// 必填配置
gb28181_agent_.setLocalAddress(local_ip_addr);
gb28181_agent_.setServerParameter(gb28181_sip_server_addr_,
gb28181_sip_server_port_, gb28181_sip_server_id_, gb28181_sip_domain_);
gb28181_agent_.setUserInfo(gb28181_sip_username_, gb28181_sip_password_);

// 可选配置
gb28181_agent_.setUserAgent(gb28181_sip_user_agent_filed_);
gb28181_agent_.setTransportProtocol(gb28181_sip_trans_protocol_ == 0 ? "UDP" : "TCP");
gb28181_agent_.config(gb28181_reg_expired_, gb28181_heartbeat_interval_, gb28181_heartbeat_count_);

// 添加设备通道(含位置信息)
com.gb.ntsignalling.Device gb_device = new com.gb.ntsignalling.Device(
gb28181_channel_id_, "安卓测试设备", Build.MANUFACTURER, Build.MODEL,
"宇宙", "火星1", "火星", true);

if (mLongitude != null && mLatitude != null) {
com.gb.ntsignalling.DevicePosition device_pos = new com.gb.ntsignalling.DevicePosition();
device_pos.setTime(mLocationTime);
device_pos.setLongitude(mLongitude);
device_pos.setLatitude(mLatitude);
gb_device.setPosition(device_pos);
gb_device.setSupportMobilePosition(true); // 支持移动位置上报
}
gb28181_agent_.addDevice(gb_device);

// 创建SIP栈
if (!gb28181_agent_.createSipStack()) {
gb28181_agent_ = null;
return false;
}

// 自动寻找可用本地端口(最多尝试5000个)
boolean is_bind_local_port_ok = false;
int try_end_port = Math.min(gb28181_sip_local_port_base_ + 5000, 65536);
for (int i = gb28181_sip_local_port_base_; i < try_end_port; ++i) {
if (gb28181_agent_.bindLocalPort(i)) {
is_bind_local_port_ok = true;
break;
}
}

if (!is_bind_local_port_ok) {
gb28181_agent_.releaseSipStack();
gb28181_agent_ = null;
return false;
}

if (!gb28181_agent_.initialize()) {
gb28181_agent_.unBindLocalPort();
gb28181_agent_.releaseSipStack();
gb28181_agent_ = null;
return false;
}

return true;
}


六、注册成功与心跳处理

6.1 注册结果回调

@Override
public void ntsRegisterOK(String dateString) {
Log.i(TAG, "ntsRegisterOK Date: " + (dateString != null ? dateString : ""));
}

@Override
public void ntsRegisterTimeout() {
Log.e(TAG, "ntsRegisterTimeout"); // 可在此触发重新注册逻辑
}

@Override
public void ntsRegisterTransportError(String errorInfo) {
Log.e(TAG, "ntsRegisterTransportError error:" + errorInfo);
}

6.2 心跳异常处理(自动重连)

GB/T 28181规定:连续心跳失败次数超过阈值时,设备应认为已与服务器断开连接,并自动尝试重新注册。Demo实现如下:

@Override
public void ntsOnHeartBeatException(int exceptionCount, String lastExceptionInfo) {
Log.e(TAG, "心跳超时, 连续失败次数:" + exceptionCount);

handler_.postDelayed(new Runnable() {
@Override
public void run() {
// 停止所有媒体流和语音广播
stopRecordDownloads(true);
stopPlaybacks(true);
stopAudioPlayer();
destoryRTPReceiver();
stopGB28181Stream();
destoryRTPSender();

if (gb28181_agent_ != null) {
gb28181_agent_.terminateAllAudioBroadcasts(true);
gb28181_agent_.terminateAllPlays(true);
gb28181_agent_.stop();

// 更新本机IP(WLAN切换后IP可能变化)
String local_ip_addr = IPAddrUtils.getIpAddress(context_);
if (local_ip_addr != null && !local_ip_addr.isEmpty()) {
gb28181_agent_.setLocalAddress(local_ip_addr);
}

// 重新初始化回放和下载监听器,然后重新start
initPlaybacks(null);
initRecordDownloads(null);
gb28181_agent_.start();
}
}
}, 0);
}


七、实时视频流推送(INVITE流程实现)

7.1 收到INVITE请求(ntsOnInvitePlay)

平台下发INVITE时,SDK会回调ntsOnInvitePlay,开发者需在此创建RTP Sender并回复200 OK:

@Override
public void ntsOnInvitePlay(String deviceId, SessionDescription session_des) {
handler_.postDelayed(new Runnable() {
@Override
public void run() {
// 振铃响应
gb28181_agent_.respondPlayInvite(180, device_id_);

// 解析SDP,找到PS视频描述
MediaSessionDescription video_des = null;
SDPRtpMapAttribute ps_rtpmap_attr = null;
Vector<MediaSessionDescription> video_des_list = session_des_.getVideoPSDescriptions();
if (video_des_list != null) {
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 || null == ps_rtpmap_attr) {
gb28181_agent_.respondPlayInvite(488, device_id_);
return;
}

// 创建RTP Sender
long rtp_sender_handle = libPublisher.CreateRTPSender(0);
if (rtp_sender_handle == 0) {
gb28181_agent_.respondPlayInvite(488, device_id_);
return;
}

gb28181_rtp_payload_type_ = ps_rtpmap_attr.getPayloadType();
gb28181_rtp_encoding_name_ = ps_rtpmap_attr.getEncodingName();

// 配置RTP Sender参数
libPublisher.SetRTPSenderTransportProtocol(rtp_sender_handle,
video_des.isRTPOverUDP() ? 0 : 1); // UDP/TCP
libPublisher.SetRTPSenderIPAddressType(rtp_sender_handle,
video_des.isIPv4() ? 0 : 1); // IPv4/IPv6
libPublisher.SetRTPSenderLocalPort(rtp_sender_handle, 0); // 0=自动分配
libPublisher.SetRTPSenderSSRC(rtp_sender_handle, video_des.getSSRC());
libPublisher.SetRTPSenderSocketSendBuffer(rtp_sender_handle, 2 * 1024 * 1024);
libPublisher.SetRTPSenderClockRate(rtp_sender_handle,
ps_rtpmap_attr.getClockRate()); // 通常90000
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);
String local_ip_addr = IPAddrUtils.getIpAddress(context_);

// 构造本端SDP
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());

// 回复200 OK,携带本端SDP
if (!gb28181_agent_.respondPlayInviteOK(device_id_, local_video_des)) {
libPublisher.DestoryRTPSender(rtp_sender_handle);
return;
}

gb28181_rtp_sender_handle_ = rtp_sender_handle;
}
// … set方法省略
}.set(deviceId, session_des), 0);
}

7.2 收到ACK后开始推流(ntsOnAckPlay)

@Override
public void ntsOnAckPlay(String deviceId) {
handler_.postDelayed(new Runnable() {
@Override
public void run() {
// 初始化Publisher(若尚未初始化)
InitAndSetConfig();

// 绑定RTP Sender
stream_publisher_.SetGB28181RTPSender(gb28181_rtp_sender_handle_,
gb28181_rtp_payload_type_, gb28181_rtp_encoding_name_);

// 启动GB28181媒体流
boolean start_ret = stream_publisher_.StartGB28181MediaStream();
if (!start_ret) {
stream_publisher_.try_release();
destoryRTPSender();
return;
}

// 启动音频采集
startAudioRecorder();

// 启动水印图层线程
startLayerPostThread();
}
// … set方法省略
}.set(deviceId), 0);
}

7.3 停止推流(BYE/Terminate)

@Override
public void ntsOnByePlay(String deviceId) {
handler_.postDelayed(new Runnable() {
@Override
public void run() {
stopGB28181Stream();
destoryRTPSender();
}
// …
}.set(deviceId), 0);
}

private void stopGB28181Stream() {
stream_publisher_.StopGB28181MediaStream();
stream_publisher_.try_release();
if (!stream_publisher_.is_publishing())
stopAudioRecorder();
}

private void destoryRTPSender() {
if (gb28181_rtp_sender_handle_ != 0) {
libPublisher.DestoryRTPSender(gb28181_rtp_sender_handle_);
gb28181_rtp_sender_handle_ = 0;
}
}


八、音频采集与PCM投递

8.1 启动音频录制

void startAudioRecorder() {
if (audio_recorder_ != null) return;

audio_recorder_ = new NTAudioRecordV2(this);
audio_recorder_callback_ = new NTAudioRecordV2CallbackImpl(stream_publisher_, null);
audio_recorder_.AddCallback(audio_recorder_callback_);

// PCMA使用8000Hz采样,AAC使用44100Hz
if (!audio_recorder_.Start(is_pcma_ ? 8000 : 44100, 1)) {
audio_recorder_.RemoveCallback(audio_recorder_callback_);
audio_recorder_callback_ = null;
audio_recorder_ = null;
}
}

8.2 PCM数据回调

private static class NTAudioRecordV2CallbackImpl implements NTAudioRecordV2Callback {
private WeakReference<LibPublisherWrapper> publisher_0_;
private WeakReference<LibPublisherWrapper> publisher_1_;

// …构造函数省略

@Override
public void onNTAudioRecordV2Frame(ByteBuffer data, int size, int sampleRate,
int channel, int per_channel_sample_number) {
// 投递给stream_publisher_(GB28181推流)
LibPublisherWrapper publisher_0 = get_publisher_0();
if (publisher_0 != null)
publisher_0.OnPCMData(data, size, sampleRate, channel, per_channel_sample_number);

// 可选:同时投递给第二个publisher(如录像publisher)
LibPublisherWrapper publisher_1 = get_publisher_1();
if (publisher_1 != null)
publisher_1.OnPCMData(data, size, sampleRate, channel, per_channel_sample_number);
}
}


九、语音广播(双向对讲)

GB/T 28181的语音广播流程是平台主动向设备发送广播指令,设备接受后,平台推送音频RTP流到设备,由设备播放。

9.1 收到广播通知

@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);
btnGB28181AudioBroadcast.setText("收到GB28181语音广播通知");
}
}
// …
}.set(fromUserName, fromUserNameAtDomain, sn, sourceID, targetID), 0);
}

9.2 INVITE音频广播

@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(用于接收平台推来的音频流)
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); // IPv4

if (0 == lib_player_.CreateRTPReceiverSession(rtp_receiver_handle_, 0)) {
int local_port = lib_player_.GetRTPReceiverLocalPort(rtp_receiver_handle_);

// 向平台发起音频INVITE
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) {
btnGB28181AudioBroadcast.setText("GB28181语音广播呼叫中");
} else {
destoryRTPReceiver();
}
}
}
}
}
// …
}.set(commandFromUserName, commandFromUserNameAtDomain, sourceID, targetID), 0);
}

9.3 处理音频广播响应并启动播放

@Override
public void ntsOnInviteAudioBroadcastResponse(String sourceID, String targetID,
int statusCode,
SessionDescription sessionDescription) {
handler_.postDelayed(new Runnable() {
@Override
public void run() {
if (gb28181_agent_ != null && 200 == status_code_
&& session_description_ != null && rtp_receiver_handle_ != 0) {
// 解析音频SDP,获取PCMA/PCMU/AAC等格式描述
MediaSessionDescription audio_des = null;
List<SDPRtpMapAttribute> audio_attrs = new LinkedList<>();
// …(解析逻辑略,参考完整源码)

if (audio_des != null && !audio_attrs.isEmpty()) {
// 设置SSRC(部分场景下SDP.SSRC与RTP.SSRC不一致时,此行可注释掉)
lib_player_.SetRTPReceiverSSRC(rtp_receiver_handle_, audio_des.getSSRC());
lib_player_.ClearRTPReceiverPayloadTypes(rtp_receiver_handle_);

for (SDPRtpMapAttribute a : audio_attrs) {
lib_player_.SetRTPReceiverPayloadType(rtp_receiver_handle_,
a.getPayloadType(), a.getEncodingName(), 2, a.getClockRate());
}

lib_player_.SetRTPReceiverRemoteAddress(rtp_receiver_handle_,
audio_des.getAddress(), audio_des.getPort());
lib_player_.InitRTPReceiver(rtp_receiver_handle_);

if (startAudioPlay()) {
gb_broadcast_source_id_ = source_id_;
gb_broadcast_target_id_ = target_id_;
btnGB28181AudioBroadcast.setText("终止GB28181语音广播");
btnGB28181AudioBroadcast.setEnabled(true);
}
}
}
}
// …
}.set(sourceID, targetID, statusCode, sessionDescription), 0);
}

9.4 音频播放器启动(含超时保护)

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); // 0缓冲,低延迟

// 绑定RTP Receiver
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); // AudioTrack模式
lib_player_.SmartPlayerSetMute(player_handle_, 0);
lib_player_.SmartPlayerSetAudioVolume(player_handle_, 100);

// 设置外部PCM输出,将收到的音频数据反馈给推流Publisher(用于回音消除链路)
lib_player_.SmartPlayerSetExternalAudioOutput(player_handle_,
new PlayerExternalPCMOutput(stream_publisher_));

lib_player_.SmartPlayerSetUrl(player_handle_, "rtp://ntinternal/rtpreceiver/implemention0");

if (0 != lib_player_.SmartPlayerStartPlay(player_handle_)) {
lib_player_.SmartPlayerClose(player_handle_);
player_handle_ = 0;
return false;
}

lib_player_.SmartPlayerSetAudioDataCallback(player_handle_, new PlayerAudioDataOutput());

if (0 == lib_player_.SmartPlayerStartPullStream(player_handle_)) {
// 启动超时保护定时器:60秒未收到音频数据则自动断开
last_received_audio_data_time_.set(SystemClock.elapsedRealtime());
handler_.postDelayed(new AudioPlayerPCMTimer(player_handle_),
AudioPlayerPCMTimer.INTERVAL_MS);
}

return true;
}

超时保护机制:AudioPlayerPCMTimer每10秒检查一次最后收到音频数据的时间,超过60秒无数据则自动发送BYE并释放资源,避免悬空连接占用系统资源。


十、水印图层叠加(LayerPostThread)

10.1 图层架构说明

SDK采用"层叠加模式"(video_opt=3)进行视频合成,支持多图层并行叠加:

图层索引用途
0 摄像头原始视频(基础层,不可删除)
1 时间戳文字水印
2 文字水印一
3 文字水印二
4 图片水印(PNG/JPEG)
5 矩形颜色块(演示Alpha渐变效果)

10.2 LayerPostThread实现

public class LayerPostThread extends Thread {
private final int update_interval = 400; // 400ms更新一次
private final AtomicBoolean is_exit_ = new AtomicBoolean(false);

private final int timestamp_index_ = 1;
private final int text1_index_ = 2;
private final int text2_index_ = 3;
private final int picture_index_ = 4;
private final int rectangle_index_ = 5;

private void on_run() {
while (!is_exit()) {
// 获取当前视频尺寸
VideoSize video_size = get_video_size();
if (null == video_size) { sleep_ms(80); continue; }

int video_w = video_size.get_width();
int video_h = video_size.get_height();

// 更新各图层
on_update_layers(publisher_list, true, video_w, video_h);

// 矩形alpha渐变演示
if (is_text_ && is_picture_) {
postRGBRectangle(publisher_list, rectangle_index_, rectangle_alpha, video_w, video_h);
rectangle_alpha = (rectangle_alpha + 8) % 256;
}

sleep_ms(update_interval – (int)cost_ms);
}
}
}

10.3 文字水印渲染

文字通过Android Canvas绘制为Bitmap后,通过PostLayerBitmap接口投递:

private int post_timestamp_layer(…) {
Bitmap text_bitmap = makeTextBitmap(makeTimestampString(), getFontSize(video_w),
Color.argb(255, 0, 0, 0), // 字体颜色:黑色
true, // 开启描边
Color.argb(255, 255, 255, 255), // 描边颜色:白色
true); // 粗体

for (LibPublisherWrapper i : publisher_list)
i.PostLayerBitmap(index, left, top, text_bitmap,
0, 0, 0, 0, // clip参数
0, 0, // 不翻转
scale_w, scale_h, scale_filter_mode, 0); // 缩放

text_bitmap.recycle();
return ret;
}

10.4 图层启停控制

// 启用/停用文字水印层
layer_post_thread_.enableText(true);
// 启用/停用图片水印层
layer_post_thread_.enablePicture(true);

// 停用某个图层(不再渲染)
for (LibPublisherWrapper i : publisher_list)
i.EnableLayer(index, false);

// 彻底移除某个图层
for (LibPublisherWrapper i : publisher_list)
i.RemoveLayer(index);


十一、云台控制(PTZ)

GB/T 28181的PTZ控制通过DeviceControl消息传递,Byte6编码云台动作(上/下/左/右/变焦等)。

@Override
public void ntsOnDeviceControlPTZCmd(String deviceId, String typeValue) {
handler_.postDelayed(new Runnable() {
@Override
public void run() {
if (null == ptz_type_ || ptz_type_.trim().length() != 16) return;

ptz_type_ = ptz_type_.trim();
// Byte6(位6~7):0=方向控制,1=变焦控制
int instruction = hexStringToInt(ptz_type_.substring(6, 8));
int combination_code2 = hexStringToInt(ptz_type_.substring(12, 14));

if ((instruction & 0xc0) == 0) {
if ((instruction & 0x20) != 0) {
// Zoom Out(缩小)
int zoom_speed = (combination_code2 >> 4) & 0xf;
Log.i(TAG, "PTZ Zoom Out, speed:" + zoom_speed);
// 此处可驱动摄像头光学变焦或切换摄像头

} else if ((instruction & 0x10) != 0) {
// Zoom In(放大)
int zoom_speed = (combination_code2 >> 4) & 0xf;
Log.i(TAG, "PTZ Zoom In, speed:" + zoom_speed);
}
// 其他方向控制(上/下/左/右)可参考GB28181标准Byte6定义
}
}
// …
}.set(deviceId, typeValue), 0);
}


十二、快照(截图上传)

12.1 快照配置响应

@Override
public void ntsOnDeviceSnapShotConfig(String from_user_name, String from_user_name_at_domain,
String sn, String device_id, SnapShotConfig config,
List<String> extra_info_list) {
handler_.postDelayed(new Runnable() {
@Override
public void run() {
if (null == snap_shot_impl_) {
snap_shot_impl_ = new SnapShotGBImpl(image_path_, context_,
handler_, libPublisher, snap_shot_publisher_);
snap_shot_impl_.start();
}

startLayerPostThread();
snap_shot_impl_.set_layer_post_thread(layer_post_thread_);

snap_shot_impl_.add_config(gb28181_agent_, from_user_name_,
from_user_name_at_domain_, sn_, device_id_,
snap_shot_config_, extra_info_list_);
}
// …
}.set(from_user_name, from_user_name_at_domain, sn, device_id, config, extra_info_list), 0);
}

12.2 手动触发截图

class ButtonCaptureImageListener implements View.OnClickListener {
public void onClick(View v) {
if (null == snap_shot_impl_) {
snap_shot_impl_ = new SnapShotGBImpl(image_path_, context_,
handler_, libPublisher, snap_shot_publisher_);
snap_shot_impl_.start();
}
startLayerPostThread();
snap_shot_impl_.set_layer_post_thread(layer_post_thread_);
snap_shot_impl_.capture();
}
}

截图支持JPEG(压缩比可控)和PNG两种格式,通过CaptureImage接口触发:

// compress_format: 0=JPEG, 1=PNG
// quality: [0,100], 仅JPEG有效
publisher.CaptureImage(0, 90, "/sdcard/snapshot_20240801.jpeg", "user_tag");


十三、录像查询与回放(历史视频)

13.1 录像查询

平台下发录像查询命令时,SDK通过ntsOnQueryRecordInfoCommand回调,Demo基于本地录像目录查询文件列表:

@Override
public void ntsOnQueryRecordInfoCommand(String fromUserName, String fromUserNameAtDomain,
String toUserName, RecordQueryInfo recordQueryInfo) {
handler_.post(new Runnable() {
@Override
public void run() {
QueryRecordInfoTask query_task = new QueryRecordInfoTask();
query_task.set(handler_, gb28181_agent_, recDir,
from_user_name_, from_user_name_at_domain_,
to_user_name_, record_query_info_);

// 提交到后台线程池异步执行,避免阻塞主线程
if (!record_executor_.submit(query_task))
Log.e(TAG, "submit QueryRecordInfoTask failed");
}
// …
}.set(fromUserName, fromUserNameAtDomain, toUserName, recordQueryInfo));
}

查询任务解析录像文件名中的时间信息(需符合特定命名规范),根据起止时间范围过滤后,通过sip_agent.respondRecordInfoQueryCommand回复查询结果。

13.2 回放流推送

历史回放与录像下载流程与实时推流类似,但SetStreamType需设置为1(on-demand流),并通过PostVideoOnDemandPacketByteBuffer和PostAudioOnDemandPacketByteBuffer按时间戳投递历史媒体包。


十四、设备生命周期管理

14.1 Activity暂停/恢复

@Override
protected void onPause() {
mOrientationDetector.disable();
if (camera2Helper != null) camera2Helper.stop();
super.onPause();
}

@Override
protected void onResume() {
super.onResume();
mOrientationDetector.enable();
if (camera2Helper != null) camera2Helper.start(video_width_, video_height_);
}

14.2 Activity销毁(完整资源释放)

@Override
protected void onDestroy() {
record_executor_.cancel_tasks();
stopRecordDownloads(true);
stopPlaybacks(true);
stopAudioPlayer();
destoryRTPReceiver();
stopAudioRecorder();

if (snap_shot_impl_ != null) {
snap_shot_impl_.stop();
snap_shot_impl_ = null;
}

snap_shot_publisher_.release();
stopPush();
stopRecorder();
stopRtspPublisher();
stopGB28181Stream();
destoryRTPSender();
stream_publisher_.release();
rtsp_server_.reset();

// UnInitRtspServer与InitRtspServer配对,只调用一次
LibPublisherWrapper.RTSPServer.deinitialize_sdk(libPublisher);
stopLayerPostThread();

if (camera2Helper != null) camera2Helper.release();

if (gb28181_agent_ != null) {
gb28181_agent_.terminateAllAudioBroadcasts(true);
gb28181_agent_.terminateAllPlays(true);
gb28181_agent_.stop();
gb28181_agent_.unInitialize();
gb28181_agent_.unBindLocalPort();
gb28181_agent_.releaseSipStack();
gb28181_agent_ = null;
}

record_executor_.shutdown(60, TimeUnit.SECONDS);
super.onDestroy();
}

关键注意事项:GB28181 SIP栈的释放顺序必须严格遵循:stop() → unInitialize() → unBindLocalPort() → releaseSipStack(),中间建议等待500ms~2000ms确保注销消息发出后再释放。


十五、LibPublisherWrapper线程安全设计

LibPublisherWrapper是对SmartPublisherJniV2的封装,核心设计亮点:

  • 读写锁保护:使用ReentrantReadWriteLock,写操作(启动/停止)加写锁,数据投递(PCM/视频帧)使用tryLock非阻塞读锁,保证高频数据投递不阻塞控制流;
  • 状态标志:is_rtmp_publishing_、is_recording_、is_rtsp_publishing_、is_gb_stream_publishing_四个volatile标志位分别跟踪推流状态;
  • AutoCloseable:实现AutoCloseable接口,支持try-with-resources;
  • finalize兜底:在finalize中检查并关闭未释放的handle,防止资源泄漏。

  • 十六、常见问题与调试建议

    问题现象可能原因解决方案
    注册超时 网络不通或SIP服务器地址/端口错误 检查网络连通性,确认服务器参数
    INVITE响应488 SDP中无有效PS视频描述 检查平台SDP配置,确认PT=96,name=PS
    视频花屏/无画面 rotation_degree未正确计算 检查方向传感器回调,确认cameraImageRotationDegree_有效
    语音广播无声音 RTP Receiver端口未打开或SSRC不匹配 打印本端port和SSRC,确认与SDP一致
    心跳后自动断开 心跳间隔设置过短或网络抖动 适当增大heartbeat_interval(建议30~60秒)
    录像文件无法查询 录像文件命名不符合解析规范 检查RecordHelper.rename_file的命名格式

    总结

    本文系统梳理了基于大牛直播SDK(SmartGBD)在Android平台实现GB28181完整接入的关键技术要点,包含从SIP信令处理、RTP媒体流推送、语音广播、云台控制到录像查询的全链路实现。代码以实际Demo为准,具有较强的工程参考价值。

    主要特性总结:

    • 多推流模式并存:GB28181、RTMP、RTSP、本地录像可并行工作
    • 层叠加水印:支持文字/图片/矩形多图层动态叠加,不影响主推流性能
    • 音视频编码灵活:软编H.264(VBR)/ 硬编H.264 / 硬编H.265按需切换
    • 音频格式兼容:支持PCMA(G.711a,GB28181最常用)和AAC
    • 自动重连:心跳异常、网络切换后自动重新注册
    • 线程安全:Publisher封装层通过读写锁保障并发安全

    📎 CSDN官方博客:音视频牛哥-CSDN博客

    赞(0)
    未经允许不得转载:171主机测评 » Android平台GB28181接入实战:基于SmartGBD实现设备注册、实时视频推送与语音广播
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址