前言
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的封装,核心设计亮点:
十六、常见问题与调试建议
| 注册超时 | 网络不通或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博客






