01 GB28181协议基础理解

GB28181协议基础理解

本文深入解析GB28181国家标准协议,从协议架构、SIP协议应用、SDP会话描述、RTP传输机制到设备注册全流程,帮助你彻底理解视频监控设备接入的底层原理。

目录

  • [1. 为什么需要GB28181协议?](#1. 为什么需要GB28181协议?)
  • [2. GB28181协议架构与分层](#2. GB28181协议架构与分层)
    • [2.1 协议整体架构](#2.1 协议整体架构)
    • [2.2 协议分层模型](#2.2 协议分层模型)
    • [2.3 核心组件关系](#2.3 核心组件关系)
  • [3. SIP协议在GB28181中的应用](#3. SIP协议在GB28181中的应用)
    • [3.1 什么是SIP协议](#3.1 什么是SIP协议)
    • [3.2 SIP消息结构](#3.2 SIP消息结构)
    • [3.3 SIP在GB28181中的作用](#3.3 SIP在GB28181中的作用)
    • [3.4 实现SIP服务端](#3.4 实现SIP服务端)
  • [4. SDP会话描述协议解析](#4. SDP会话描述协议解析)
    • [4.1 SDP协议概述](#4.1 SDP协议概述)
    • [4.2 SDP字段详解](#4.2 SDP字段详解)
    • [4.3 SDP解析实现](#4.3 SDP解析实现)
  • [5. RTP/RTCP传输协议机制](#5. RTP/RTCP传输协议机制)
    • [5.1 RTP协议原理](#5.1 RTP协议原理)
    • [5.2 RTCP控制协议](#5.2 RTCP控制协议)
    • [5.3 RTP接收器实现](#5.3 RTP接收器实现)
  • [6. GB28181设备注册流程](#6. GB28181设备注册流程)
    • [6.1 注册时序图](#6.1 注册时序图)
    • [6.2 注册认证机制](#6.2 注册认证机制)
    • [6.3 注册流程实现](#6.3 注册流程实现)
  • [7. 常见坑点与最佳实践](#7. 常见坑点与最佳实践)
  • [8. 面试高频题](#8. 面试高频题)

1. 为什么需要GB28181协议?

业务痛点

在智慧城市、平安城市等大规模视频监控项目中,经常会遇到这些问题:

多厂商设备接入难题:

  • 海康、大华、宇视等不同厂商的设备协议各不相同
  • 每个厂商都需要单独对接SDK,开发成本极高
  • SDK通常是C/C++开发,跨平台困难,Web端几乎无法使用
  • 厂商SDK版本升级频繁,维护成本巨大

实际案例:

某城市项目需要接入15个厂家的10000+摄像头,如果每个厂家单独对接:

复制代码
15个厂家 × 平均30人/天开发 × 平均8000元/天 = 360万元
后期维护:15个厂家 × 2人/厂家 × 20000元/月 × 12月 = 720万元/年

使用GB28181统一接入:

复制代码
统一协议开发:5人 × 60天 × 8000元 = 240万元
后期维护:2人 × 20000元/月 × 12月 = 48万元/年
节省成本:(360 + 720) - (240 + 48) = 792万元

GB28181协议的价值

GB28181(全称:公共安全视频监控联网系统信息传输、交换、控制技术要求)是国家强制标准,主要解决:

  1. 统一设备接入标准

    • 所有支持GB28181的设备,使用相同的协议
    • 一套代码对接所有厂家设备
    • 大幅降低开发和维护成本
  2. 跨平台兼容性

    • 基于标准网络协议(SIP/RTP/RTSP)
    • 支持Java、Python、Go等多种语言实现
    • Web端可通过WebRTC或HLS播放
  3. 扩展性强

    • 支持设备注册、心跳保活、目录查询
    • 支持实时视频、历史回放、云台控制
    • 支持级联(上下级平台互联)
  4. 国家强制要求

    • 公安、交通等行业强制使用
    • 政府项目必须支持GB28181

2. GB28181协议架构与分层

2.1 协议整体架构

GB28181协议采用分层架构,各层职责明确:

复制代码
┌──────────────────────────────────────────────────────────┐
│                      应用层                               │
│  (目录查询、实时视频、历史回放、云台控制、报警订阅)        │
└──────────────────────────────────────────────────────────┘
                            ↓
┌──────────────────────────────────────────────────────────┐
│                     信令控制层                            │
│  SIP协议 (设备注册、心跳保活、会话建立、状态查询)         │
└──────────────────────────────────────────────────────────┘
                            ↓
┌──────────────────────────────────────────────────────────┐
│                     会话描述层                            │
│  SDP协议 (媒体参数协商、编码格式、传输地址)               │
└──────────────────────────────────────────────────────────┘
                            ↓
┌──────────────────────────────────────────────────────────┐
│                     媒体传输层                            │
│  RTP/RTCP协议 (音视频数据传输、质量反馈)                  │
└──────────────────────────────────────────────────────────┘
                            ↓
┌──────────────────────────────────────────────────────────┐
│                      网络层                               │
│  TCP/UDP协议                                             │
└──────────────────────────────────────────────────────────┘

为什么要分层?

  • 职责分离:每一层只关注自己的事情,信令归信令,媒体归媒体
  • 可扩展性:想换传输协议(比如用RTMP),只改媒体层,不影响信令层
  • 易维护:出了问题能快速定位是哪一层的问题

2.2 协议分层模型

第一层:信令控制层(SIP协议)

SIP(Session Initiation Protocol)会话发起协议,负责建立、修改、终止会话。

核心功能:

  • 设备注册(REGISTER)
  • 会话邀请(INVITE)
  • 会话确认(ACK)
  • 会话结束(BYE)
  • 消息传递(MESSAGE)

为什么选择SIP而不是自定义协议?

  • SIP是成熟的互联网标准(RFC 3261)
  • 有大量开源库支持(JAIN-SIP、PjSIP、EXosip)
  • 文本协议,易于调试和分析
  • 支持NAT穿透、代理、认证等复杂场景

第二层:会话描述层(SDP协议)

SDP(Session Description Protocol)会话描述协议,用于描述会话的媒体参数。

核心作用:

  • 描述媒体类型(音频/视频)
  • 指定编码格式(H.264/H.265/AAC)
  • 定义传输地址(IP和端口)
  • 声明媒体属性(码率、分辨率)

为什么需要SDP?

  • 设备能力千差万别,需要协商统一的媒体参数
  • 动态分配端口,支持多路并发
  • 双方确认参数后才开始传输,避免资源浪费

第三层:媒体传输层(RTP/RTCP协议)

RTP(Real-time Transport Protocol)实时传输协议,用于传输音视频数据。

RTP核心特性:

  • 时间戳:用于音视频同步
  • 序列号:检测丢包和乱序
  • 载荷类型:标识编码格式
  • 同步源:区分不同的媒体流

RTCP(RTP Control Protocol)控制协议:

  • 监控传输质量(丢包率、抖动)
  • 提供发送端反馈
  • 用于会话控制和同步

为什么不直接传原始流?

  • 需要时间戳同步
  • 需要序列号检测丢包
  • 需要标识载荷类型
  • 需要质量反馈机制

2.3 核心组件关系

在实际项目中,GB28181的实现通常包含以下核心组件:

复制代码
┌─────────────────────────────────────────────────────────────┐
│                        平台侧                                │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌──────────────┐      ┌──────────────┐                    │
│  │  SIP Server  │◄────►│ 会话管理器    │                    │
│  │  (信令处理)   │      │ (Session Mgr) │                    │
│  └──────────────┘      └──────────────┘                    │
│         ↓                      ↓                            │
│  ┌──────────────┐      ┌──────────────┐                    │
│  │  消息处理器   │      │  设备管理器   │                    │
│  │  (Handler)   │      │ (Device Mgr) │                    │
│  └──────────────┘      └──────────────┘                    │
│         ↓                      ↓                            │
│  ┌──────────────────────────────────┐                      │
│  │       RTP接收器                   │                      │
│  │  (接收媒体流,转发到流媒体服务器)  │                      │
│  └──────────────────────────────────┘                      │
│         ↓                                                   │
│  ┌──────────────────────────────────┐                      │
│  │     流媒体服务器                   │                      │
│  │  (ZLMediaKit/SRS/Wowza)          │                      │
│  │  (转码、推流、HLS/FLV/WebRTC)      │                      │
│  └──────────────────────────────────┘                      │
└─────────────────────────────────────────────────────────────┘
                          ↑
                    SIP/RTP协议
                          ↓
┌─────────────────────────────────────────────────────────────┐
│                        设备侧                                │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌──────────────┐      ┌──────────────┐                    │
│  │  SIP Client  │      │  媒体编码器   │                    │
│  │  (设备端实现) │      │  (H.264/265)  │                    │
│  └──────────────┘      └──────────────┘                    │
│         ↓                      ↓                            │
│  ┌──────────────────────────────────┐                      │
│  │       RTP发送器                   │                      │
│  │  (发送媒体流)                     │                      │
│  └──────────────────────────────────┘                      │
│         ↓                                                   │
│  ┌──────────────────────────────────┐                      │
│  │       摄像头硬件                  │                      │
│  │  (采集视频)                       │                      │
│  └──────────────────────────────────┘                      │
└─────────────────────────────────────────────────────────────┘

为什么需要这么多组件?

  • SIP Server:处理信令,不关心媒体流
  • 会话管理器:管理多路并发视频会话
  • RTP接收器:专门处理媒体流,避免SIP Server性能瓶颈
  • 流媒体服务器:负责转码、分发,让Web端能播放

3. SIP协议在GB28181中的应用

3.1 什么是SIP协议

SIP(Session Initiation Protocol)是一个应用层的信令协议,用于创建、修改和终止会话。

SIP的核心特点:

  • 文本协议:类似HTTP,易于调试
  • 请求-响应模型:Client发请求,Server回响应
  • 无状态:每个请求都是独立的
  • 可扩展:支持自定义头域和方法

SIP不负责媒体传输:SIP只管"打电话"(建立连接),不管"说什么"(传输内容)。

3.2 SIP消息结构

SIP请求消息格式:

复制代码
REGISTER sip:10.10.10.10:5060 SIP/2.0          ← 请求行(方法 URI 版本)
Via: SIP/2.0/UDP 10.10.10.20:5060;branch=xxx   ← Via头(路由信息)
From: <sip:34020000001320000001@10.10.10.20>;tag=xxx  ← From头(发送方)
To: <sip:34020000001320000001@10.10.10.20>     ← To头(接收方)
Call-ID: xxx                                    ← Call-ID(会话标识)
CSeq: 1 REGISTER                                ← CSeq(序列号)
Contact: <sip:34020000001320000001@10.10.10.20:5060>  ← Contact(联系地址)
Max-Forwards: 70                                ← Max-Forwards(最大跳数)
Expires: 3600                                   ← Expires(过期时间)
Content-Length: 0                               ← Content-Length(消息体长度)

                                                ← 空行分隔头部和消息体
                                                ← 消息体(可选)

SIP响应消息格式:

复制代码
SIP/2.0 200 OK                                  ← 状态行(版本 状态码 原因短语)
Via: SIP/2.0/UDP 10.10.10.20:5060;branch=xxx   ← Via头(原样返回)
From: <sip:34020000001320000001@10.10.10.20>;tag=xxx  ← From头(原样返回)
To: <sip:34020000001320000001@10.10.10.20>;tag=yyy    ← To头(添加tag)
Call-ID: xxx                                    ← Call-ID(原样返回)
CSeq: 1 REGISTER                                ← CSeq(原样返回)
Contact: <sip:34020000001320000001@10.10.10.20:5060>
Expires: 3600
Content-Length: 0

关键字段说明:

字段 作用 示例
Via 记录消息路径,响应时原路返回 SIP/2.0/UDP 10.10.10.20:5060
From 发起者地址,tag用于标识会话 <sip:user@domain>;tag=xxx
To 接收者地址 <sip:user@domain>
Call-ID 全局唯一的会话标识 xxx@10.10.10.20
CSeq 序列号+方法名,检测重传和乱序 1 REGISTER
Contact 实际联系地址,可能与From不同 <sip:user@ip:port>

3.3 SIP在GB28181中的作用

GB28181使用SIP协议完成以下功能:

  1. 设备注册(REGISTER)

    • 设备向平台注册,建立通信通道
    • 平台返回认证挑战,设备携带凭证重新注册
    • 注册成功后,设备进入在线状态
  2. 心跳保活(MESSAGE)

    • 设备定期发送心跳消息,证明在线
    • 平台检测心跳超时,标记设备离线
  3. 视频邀请(INVITE)

    • 平台向设备发起视频点播请求
    • 设备返回SDP,告知媒体参数
    • 双方建立RTP会话,开始传输视频
  4. 会话结束(BYE)

    • 平台或设备主动结束视频会话
    • 释放资源,停止传输
  5. 目录查询(MESSAGE)

    • 平台向设备查询通道列表
    • 设备返回XML格式的通道信息

3.4 实现SIP服务端

下面通过实际代码演示如何实现一个GB28181的SIP服务端。

Maven依赖:

xml 复制代码
<dependency>
    <groupId>javax.sip</groupId>
    <artifactId>jain-sip-ri</artifactId>
    <version>1.3.0-91</version>
</dependency>

SIP配置类:

java 复制代码
package com.video.platform.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * SIP协议配置
 * 
 * 配置SIP服务端的IP、端口、域、认证等参数
 */
@Data
@Component
@ConfigurationProperties(prefix = "sip.server")
public class SipConfig {
    
    /**
     * SIP服务端IP地址
     * 注意:需要配置为服务器的实际IP,不能用127.0.0.1
     */
    private String ip = "10.10.10.10";
    
    /**
     * SIP服务端端口
     * 默认5060,可根据实际情况调整
     */
    private Integer port = 5060;
    
    /**
     * SIP域
     * 通常设置为服务器IP或域名
     */
    private String domain = "10.10.10.10";
    
    /**
     * SIP服务端ID
     * 20位国标编码:中心编码(8位)+ 行业编码(2位)+ 类型编码(3位)+ 网络标识(7位)
     */
    private String serverId = "34020000002000000001";
    
    /**
     * 传输协议:UDP/TCP
     * GB28181通常使用UDP
     */
    private String transport = "UDP";
    
    /**
     * 认证用户名
     * 设备注册时使用
     */
    private String username = "admin";
    
    /**
     * 认证密码
     * 设备注册时使用
     */
    private String password = "123456";
    
    /**
     * 心跳周期(秒)
     * 设备需要在此周期内发送心跳
     */
    private Integer keepaliveInterval = 60;
    
    /**
     * 心跳超时时间(秒)
     * 超过此时间未收到心跳,标记设备离线
     */
    private Integer keepaliveTimeout = 180;
    
    /**
     * 注册有效期(秒)
     * 设备注册后的有效时间
     */
    private Integer registerExpires = 3600;
}

SIP服务端启动类:

java 复制代码
package com.video.platform.sip;

import gov.nist.javax.sip.SipStackExt;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import javax.sip.*;
import javax.sip.address.AddressFactory;
import javax.sip.header.HeaderFactory;
import javax.sip.message.MessageFactory;
import java.util.Properties;

/**
 * SIP服务端启动器
 * 
 * 基于JAIN-SIP实现GB28181的SIP服务端
 * 负责监听SIP消息,分发给对应的处理器
 */
@Slf4j
@Component
public class SipServer implements CommandLineRunner {
    
    @Autowired
    private SipConfig sipConfig;
    
    @Autowired
    private SipMessageProcessor sipMessageProcessor;
    
    private SipStack sipStack;
    private SipProvider sipProvider;
    private AddressFactory addressFactory;
    private HeaderFactory headerFactory;
    private MessageFactory messageFactory;
    
    @Override
    public void run(String... args) throws Exception {
        log.info("======== SIP服务端启动 ========");
        log.info("SIP服务端配置:");
        log.info("  - IP: {}", sipConfig.getIp());
        log.info("  - 端口: {}", sipConfig.getPort());
        log.info("  - 域: {}", sipConfig.getDomain());
        log.info("  - 服务端ID: {}", sipConfig.getServerId());
        log.info("  - 传输协议: {}", sipConfig.getTransport());
        
        // 创建SIP工厂
        SipFactory sipFactory = SipFactory.getInstance();
        sipFactory.setPathName("gov.nist");
        
        // 配置SIP Stack
        Properties properties = new Properties();
        properties.setProperty("javax.sip.STACK_NAME", "GB28181-Platform");
        properties.setProperty("javax.sip.IP_ADDRESS", sipConfig.getIp());
        
        // 调试模式(生产环境关闭)
        properties.setProperty("gov.nist.javax.sip.TRACE_LEVEL", "32");
        properties.setProperty("gov.nist.javax.sip.SERVER_LOG", "sip_server.log");
        properties.setProperty("gov.nist.javax.sip.DEBUG_LOG", "sip_debug.log");
        
        // 创建SIP Stack
        sipStack = sipFactory.createSipStack(properties);
        
        // 创建工厂
        headerFactory = sipFactory.createHeaderFactory();
        addressFactory = sipFactory.createAddressFactory();
        messageFactory = sipFactory.createMessageFactory();
        
        // 创建监听点(ListeningPoint)
        ListeningPoint listeningPoint = sipStack.createListeningPoint(
            sipConfig.getIp(), 
            sipConfig.getPort(), 
            sipConfig.getTransport()
        );
        
        // 创建SIP Provider
        sipProvider = sipStack.createSipProvider(listeningPoint);
        
        // 添加消息监听器
        sipProvider.addSipListener(sipMessageProcessor);
        
        // 启动SIP Stack
        ((SipStackExt) sipStack).start();
        
        log.info("======== SIP服务端启动成功 ========");
    }
    
    /**
     * 获取SipProvider
     * 用于发送SIP消息
     */
    public SipProvider getSipProvider() {
        return sipProvider;
    }
    
    /**
     * 获取HeaderFactory
     * 用于创建SIP头部
     */
    public HeaderFactory getHeaderFactory() {
        return headerFactory;
    }
    
    /**
     * 获取AddressFactory
     * 用于创建SIP地址
     */
    public AddressFactory getAddressFactory() {
        return addressFactory;
    }
    
    /**
     * 获取MessageFactory
     * 用于创建SIP消息
     */
    public MessageFactory getMessageFactory() {
        return messageFactory;
    }
    
    /**
     * 关闭SIP服务端
     */
    public void shutdown() {
        if (sipProvider != null) {
            sipProvider.removeSipListener(sipMessageProcessor);
            try {
                sipStack.deleteSipProvider(sipProvider);
            } catch (Exception e) {
                log.error("删除SIP Provider失败", e);
            }
        }
        
        if (sipStack != null) {
            sipStack.stop();
        }
        
        log.info("SIP服务端已关闭");
    }
}

SIP消息处理器:

java 复制代码
package com.video.platform.sip;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.sip.*;
import javax.sip.message.Request;
import javax.sip.message.Response;

/**
 * SIP消息处理器
 * 
 * 实现SipListener接口,处理各种SIP事件
 */
@Slf4j
@Component
public class SipMessageProcessor implements SipListener {
    
    @Autowired
    private SipRequestHandler sipRequestHandler;
    
    /**
     * 处理SIP请求
     */
    @Override
    public void processRequest(RequestEvent requestEvent) {
        try {
            Request request = requestEvent.getRequest();
            String method = request.getMethod();
            
            log.info("收到SIP请求:{}", method);
            
            // 分发到对应的处理器
            switch (method) {
                case Request.REGISTER:
                    sipRequestHandler.handleRegister(requestEvent);
                    break;
                case Request.MESSAGE:
                    sipRequestHandler.handleMessage(requestEvent);
                    break;
                case Request.INVITE:
                    sipRequestHandler.handleInvite(requestEvent);
                    break;
                case Request.ACK:
                    sipRequestHandler.handleAck(requestEvent);
                    break;
                case Request.BYE:
                    sipRequestHandler.handleBye(requestEvent);
                    break;
                default:
                    log.warn("不支持的请求方法:{}", method);
                    sendResponse(requestEvent, Response.NOT_IMPLEMENTED);
            }
            
        } catch (Exception e) {
            log.error("处理SIP请求异常", e);
        }
    }
    
    /**
     * 处理SIP响应
     */
    @Override
    public void processResponse(ResponseEvent responseEvent) {
        try {
            Response response = responseEvent.getResponse();
            int statusCode = response.getStatusCode();
            
            log.info("收到SIP响应:{} {}", statusCode, response.getReasonPhrase());
            
            // 根据状态码处理不同的响应
            // GB28181中,平台主要作为Server,很少处理响应
            
        } catch (Exception e) {
            log.error("处理SIP响应异常", e);
        }
    }
    
    /**
     * 超时事件
     */
    @Override
    public void processTimeout(TimeoutEvent timeoutEvent) {
        log.warn("SIP消息超时");
    }
    
    /**
     * IO异常事件
     */
    @Override
    public void processIOException(IOExceptionEvent exceptionEvent) {
        log.error("SIP IO异常:{}", exceptionEvent.getHost());
    }
    
    /**
     * 事务终止事件
     */
    @Override
    public void processTransactionTerminated(TransactionTerminatedEvent transactionTerminatedEvent) {
        log.debug("SIP事务终止");
    }
    
    /**
     * Dialog终止事件
     */
    @Override
    public void processDialogTerminated(DialogTerminatedEvent dialogTerminatedEvent) {
        log.debug("SIP Dialog终止");
    }
    
    /**
     * 发送响应
     */
    private void sendResponse(RequestEvent requestEvent, int statusCode) {
        try {
            ServerTransaction serverTransaction = requestEvent.getServerTransaction();
            if (serverTransaction == null) {
                SipProvider sipProvider = (SipProvider) requestEvent.getSource();
                serverTransaction = sipProvider.getNewServerTransaction(requestEvent.getRequest());
            }
            
            Response response = requestEvent.getDialog().createResponse(statusCode);
            serverTransaction.sendResponse(response);
            
        } catch (Exception e) {
            log.error("发送SIP响应失败", e);
        }
    }
}

4. SDP会话描述协议解析

4.1 SDP协议概述

SDP(Session Description Protocol)是一个文本协议,用于描述多媒体会话的参数。

为什么需要SDP?

在建立RTP会话之前,双方需要知道:

  • 对方支持哪些编码格式?(H.264? H.265? AAC?)
  • 使用哪个IP地址和端口接收数据?
  • 码率、分辨率是多少?
  • 音视频如何同步?

SDP就是用来交换这些信息的"协议说明书"。

SDP在GB28181中的使用场景:

  • 设备返回SDP,告知平台媒体参数
  • 平台发送SDP,告知设备推流地址

4.2 SDP字段详解

典型的GB28181 SDP示例:

复制代码
v=0                                                    ← 版本号
o=34020000001320000001 0 0 IN IP4 10.10.10.20        ← 会话所有者
s=Play                                                 ← 会话名称
c=IN IP4 10.10.10.20                                  ← 连接信息
t=0 0                                                  ← 时间描述
m=video 15060 RTP/AVP 96 98 97                        ← 媒体描述
a=rtpmap:96 PS/90000                                  ← 载荷96:PS流
a=rtpmap:98 H264/90000                                ← 载荷98:H.264
a=rtpmap:97 H265/90000                                ← 载荷97:H.265
a=recvonly                                             ← 接收方向
a=setup:passive                                        ← 连接角色
y=0100000001                                           ← SSRC(自定义字段)

字段详解:

字段 含义 示例 说明
v= 版本 v=0 SDP版本,目前固定为0
o= 会话所有者 o=username session_id version network_type address_type address 标识会话创建者
s= 会话名称 s=Play 会话的描述性名称
c= 连接信息 c=IN IP4 10.10.10.20 媒体流的目标地址
t= 时间描述 t=0 0 会话的起始和结束时间,0 0表示永久
m= 媒体描述 m=video 15060 RTP/AVP 96 媒体类型、端口、协议、载荷类型
a=rtpmap 载荷映射 a=rtpmap:96 PS/90000 载荷类型对应的编码格式和时钟频率
a=recvonly 接收方向 a=recvonly 只接收不发送
a=sendonly 发送方向 a=sendonly 只发送不接收
a=sendrecv 双向 a=sendrecv 既发送又接收
y= SSRC y=0100000001 GB28181自定义字段,标识同步源

关键点解释:

1. 媒体描述(m=)

复制代码
m=video 15060 RTP/AVP 96 98 97
  │     │      │       └─ 载荷类型列表(可以有多个,表示支持多种编码)
  │     │      └─ 传输协议(RTP/AVP表示RTP over UDP)
  │     └─ 端口号(RTP接收端口)
  └─ 媒体类型(video/audio)

2. 载荷映射(a=rtpmap)

复制代码
a=rtpmap:96 PS/90000
         │   │  └─ 时钟频率(Hz),视频通常是90000
         │   └─ 编码格式(PS/H264/H265/AAC)
         └─ 载荷类型(对应m=行中的数字)

PS流(载荷96)是什么?

  • PS(Program Stream)是MPEG-2的一种封装格式
  • GB28181强制要求使用PS流封装
  • PS流可以包含多路音视频,适合录像回放
  • 需要解析PS头,提取H.264/H.265裸流

4.3 SDP解析实现

SDP解析器:

java 复制代码
package com.video.platform.sip.sdp;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * SDP解析器
 * 
 * 解析SIP消息中的SDP部分,提取媒体参数
 */
@Slf4j
public class SdpParser {
    
    /**
     * 解析SDP
     * 
     * @param sdpContent SDP内容
     * @return SDP信息对象
     */
    public static SdpInfo parse(String sdpContent) {
        if (sdpContent == null || sdpContent.trim().isEmpty()) {
            throw new IllegalArgumentException("SDP内容为空");
        }
        
        SdpInfo sdpInfo = new SdpInfo();
        String[] lines = sdpContent.split("\r\n|\n");
        
        MediaInfo currentMedia = null;
        
        for (String line : lines) {
            if (line.trim().isEmpty()) {
                continue;
            }
            
            // SDP格式:<type>=<value>
            if (line.length() < 2 || line.charAt(1) != '=') {
                log.warn("非法的SDP行:{}", line);
                continue;
            }
            
            char type = line.charAt(0);
            String value = line.substring(2);
            
            switch (type) {
                case 'v':  // 版本
                    sdpInfo.setVersion(value);
                    break;
                    
                case 'o':  // 会话所有者
                    parseOrigin(value, sdpInfo);
                    break;
                    
                case 's':  // 会话名称
                    sdpInfo.setSessionName(value);
                    break;
                    
                case 'c':  // 连接信息
                    parseConnection(value, sdpInfo);
                    break;
                    
                case 't':  // 时间描述
                    parseTime(value, sdpInfo);
                    break;
                    
                case 'm':  // 媒体描述
                    currentMedia = parseMedia(value);
                    if (currentMedia != null) {
                        sdpInfo.getMediaList().add(currentMedia);
                    }
                    break;
                    
                case 'a':  // 属性
                    if (currentMedia != null) {
                        parseAttribute(value, currentMedia);
                    }
                    break;
                    
                case 'y':  // SSRC(GB28181自定义)
                    if (currentMedia != null) {
                        currentMedia.setSsrc(value);
                    }
                    break;
                    
                default:
                    log.debug("未处理的SDP类型:{}", type);
            }
        }
        
        return sdpInfo;
    }
    
    /**
     * 解析会话所有者(o=)
     * 格式:o=<username> <session id> <version> <network type> <address type> <address>
     */
    private static void parseOrigin(String value, SdpInfo sdpInfo) {
        String[] parts = value.split(" ");
        if (parts.length >= 6) {
            sdpInfo.setUsername(parts[0]);
            sdpInfo.setSessionId(parts[1]);
            sdpInfo.setSessionVersion(parts[2]);
            sdpInfo.setAddressType(parts[4]);
            sdpInfo.setAddress(parts[5]);
        }
    }
    
    /**
     * 解析连接信息(c=)
     * 格式:c=<network type> <address type> <connection address>
     */
    private static void parseConnection(String value, SdpInfo sdpInfo) {
        String[] parts = value.split(" ");
        if (parts.length >= 3) {
            sdpInfo.setConnectionAddress(parts[2]);
        }
    }
    
    /**
     * 解析时间描述(t=)
     * 格式:t=<start time> <stop time>
     */
    private static void parseTime(String value, SdpInfo sdpInfo) {
        String[] parts = value.split(" ");
        if (parts.length >= 2) {
            sdpInfo.setStartTime(parts[0]);
            sdpInfo.setStopTime(parts[1]);
        }
    }
    
    /**
     * 解析媒体描述(m=)
     * 格式:m=<media> <port> <protocol> <payload types>
     */
    private static MediaInfo parseMedia(String value) {
        String[] parts = value.split(" ");
        if (parts.length < 4) {
            log.warn("非法的媒体描述:{}", value);
            return null;
        }
        
        MediaInfo media = new MediaInfo();
        media.setMediaType(parts[0]);  // video/audio
        
        try {
            media.setPort(Integer.parseInt(parts[1]));
        } catch (NumberFormatException e) {
            log.error("解析端口失败:{}", parts[1]);
            return null;
        }
        
        media.setProtocol(parts[2]);  // RTP/AVP
        
        // 解析载荷类型列表
        List<Integer> payloadTypes = new ArrayList<>();
        for (int i = 3; i < parts.length; i++) {
            try {
                payloadTypes.add(Integer.parseInt(parts[i]));
            } catch (NumberFormatException e) {
                log.warn("解析载荷类型失败:{}", parts[i]);
            }
        }
        media.setPayloadTypes(payloadTypes);
        
        return media;
    }
    
    /**
     * 解析属性(a=)
     */
    private static void parseAttribute(String value, MediaInfo media) {
        if (value.startsWith("rtpmap:")) {
            // 解析载荷映射:rtpmap:<payload type> <encoding name>/<clock rate>
            String mapValue = value.substring(7);  // 去掉"rtpmap:"
            String[] parts = mapValue.split(" ", 2);
            if (parts.length == 2) {
                try {
                    int payloadType = Integer.parseInt(parts[0]);
                    String[] codecParts = parts[1].split("/");
                    if (codecParts.length >= 2) {
                        String codecName = codecParts[0];
                        int clockRate = Integer.parseInt(codecParts[1]);
                        media.addPayloadMap(payloadType, codecName, clockRate);
                    }
                } catch (NumberFormatException e) {
                    log.warn("解析rtpmap失败:{}", value);
                }
            }
        } else if (value.equals("recvonly")) {
            media.setDirection("recvonly");
        } else if (value.equals("sendonly")) {
            media.setDirection("sendonly");
        } else if (value.equals("sendrecv")) {
            media.setDirection("sendrecv");
        } else if (value.startsWith("setup:")) {
            media.setSetup(value.substring(6));
        }
    }
    
    /**
     * SDP信息
     */
    @Data
    public static class SdpInfo {
        private String version;            // v=
        private String username;           // o= 用户名
        private String sessionId;          // o= 会话ID
        private String sessionVersion;     // o= 会话版本
        private String addressType;        // o= 地址类型
        private String address;            // o= 地址
        private String sessionName;        // s=
        private String connectionAddress;  // c= 连接地址
        private String startTime;          // t= 开始时间
        private String stopTime;           // t= 结束时间
        
        private List<MediaInfo> mediaList = new ArrayList<>();  // 媒体列表
        
        /**
         * 获取第一个视频媒体
         */
        public MediaInfo getVideoMedia() {
            return mediaList.stream()
                .filter(m -> "video".equals(m.getMediaType()))
                .findFirst()
                .orElse(null);
        }
        
        /**
         * 获取第一个音频媒体
         */
        public MediaInfo getAudioMedia() {
            return mediaList.stream()
                .filter(m -> "audio".equals(m.getMediaType()))
                .findFirst()
                .orElse(null);
        }
    }
    
    /**
     * 媒体信息
     */
    @Data
    public static class MediaInfo {
        private String mediaType;          // video/audio
        private Integer port;              // 端口
        private String protocol;           // RTP/AVP
        private List<Integer> payloadTypes = new ArrayList<>();  // 载荷类型列表
        
        private Map<Integer, PayloadMap> payloadMaps = new HashMap<>();  // 载荷映射
        
        private String direction;          // recvonly/sendonly/sendrecv
        private String setup;              // active/passive
        private String ssrc;               // SSRC
        
        /**
         * 添加载荷映射
         */
        public void addPayloadMap(int payloadType, String codecName, int clockRate) {
            PayloadMap map = new PayloadMap();
            map.setPayloadType(payloadType);
            map.setCodecName(codecName);
            map.setClockRate(clockRate);
            payloadMaps.put(payloadType, map);
        }
        
        /**
         * 获取第一个载荷映射
         */
        public PayloadMap getFirstPayloadMap() {
            return payloadTypes.isEmpty() ? null : payloadMaps.get(payloadTypes.get(0));
        }
        
        /**
         * 判断是否支持PS流
         */
        public boolean isSupportPS() {
            return payloadMaps.values().stream()
                .anyMatch(m -> "PS".equalsIgnoreCase(m.getCodecName()));
        }
        
        /**
         * 判断是否支持H.264
         */
        public boolean isSupportH264() {
            return payloadMaps.values().stream()
                .anyMatch(m -> "H264".equalsIgnoreCase(m.getCodecName()));
        }
    }
    
    /**
     * 载荷映射
     */
    @Data
    public static class PayloadMap {
        private Integer payloadType;       // 载荷类型
        private String codecName;          // 编码名称(PS/H264/H265/AAC)
        private Integer clockRate;         // 时钟频率
    }
}

使用示例:

java 复制代码
String sdpContent = "v=0\r\n" +
    "o=34020000001320000001 0 0 IN IP4 10.10.10.20\r\n" +
    "s=Play\r\n" +
    "c=IN IP4 10.10.10.20\r\n" +
    "t=0 0\r\n" +
    "m=video 15060 RTP/AVP 96 98\r\n" +
    "a=rtpmap:96 PS/90000\r\n" +
    "a=rtpmap:98 H264/90000\r\n" +
    "a=recvonly\r\n" +
    "y=0100000001\r\n";

SdpParser.SdpInfo sdpInfo = SdpParser.parse(sdpContent);

// 获取视频媒体信息
SdpParser.MediaInfo video = sdpInfo.getVideoMedia();
System.out.println("视频端口:" + video.getPort());  // 15060
System.out.println("传输协议:" + video.getProtocol());  // RTP/AVP
System.out.println("支持PS流:" + video.isSupportPS());  // true
System.out.println("支持H.264:" + video.isSupportH264());  // true
System.out.println("SSRC:" + video.getSsrc());  // 0100000001

5. RTP/RTCP传输协议机制

5.1 RTP协议原理

RTP(Real-time Transport Protocol)是为实时传输音视频设计的传输层协议。

为什么要用RTP而不是直接用UDP?

UDP只负责传输数据包,不关心内容,而视频传输需要:

  • 时间戳:音视频同步
  • 序列号:检测丢包和乱序
  • 载荷类型标识:区分编码格式
  • 同步源标识:区分不同的媒体流

RTP在UDP的基础上增加了这些功能。

RTP包结构:

复制代码
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P|X|  CC   |M|     PT      |       sequence number         |  ← RTP固定头(12字节)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           timestamp                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           synchronization source (SSRC) identifier            |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|            contributing source (CSRC) identifiers             |  ← 可选的CSRC列表
|                             ....                              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      RTP Extension (可选)                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|                       Payload (媒体数据)                       |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

字段说明:

字段 位数 说明
V (Version) 2 版本号,固定为2
P (Padding) 1 填充标志,用于对齐
X (Extension) 1 扩展头标志
CC (CSRC Count) 4 CSRC数量(0-15)
M (Marker) 1 标记位,视频帧结束标志
PT (Payload Type) 7 载荷类型(对应SDP中的载荷类型)
Sequence Number 16 序列号,每发送一个包+1
Timestamp 32 时间戳,用于音视频同步
SSRC 32 同步源标识,唯一标识一个媒体流

关键字段详解:

1. 序列号(Sequence Number)

  • 每发送一个RTP包,序列号+1
  • 接收端通过序列号检测丢包和乱序
  • 序列号从随机值开始,避免预测攻击

2. 时间戳(Timestamp)

  • 表示RTP包中第一个字节的采样时刻
  • 时钟频率由SDP中的clockRate指定(视频通常是90000Hz)
  • 用于音视频同步和计算播放时间

示例:

复制代码
假设时钟频率90000Hz,视频帧率30fps
则每帧的时间戳增量 = 90000 / 30 = 3000

第1帧:timestamp = 0
第2帧:timestamp = 3000
第3帧:timestamp = 6000

3. 载荷类型(Payload Type)

  • 标识RTP包中的媒体格式
  • 对应SDP中的载荷类型
  • GB28181中:96=PS流,98=H.264,97=H.265

4. SSRC(同步源标识)

  • 唯一标识一个RTP流
  • 用于区分不同的媒体流(多路并发时)
  • 随机生成,避免冲突

5. Marker位(M)

  • 视频:标识帧的结束
  • 音频:标识通话的开始

5.2 RTCP控制协议

RTCP(RTP Control Protocol)是RTP的配套协议,用于监控传输质量和会话控制。

RTCP的作用:

  • 监控RTP传输质量(丢包率、抖动、延迟)
  • 提供发送端反馈(让发送端调整码率)
  • 传输会话参数(如CNAME)

RTCP包类型:

类型 名称 说明
SR (200) Sender Report 发送端报告(发送统计)
RR (201) Receiver Report 接收端报告(接收统计)
SDES (202) Source Description 源描述(CNAME等)
BYE (203) Goodbye 会话结束通知
APP (204) Application 应用自定义

RTCP端口规则:

  • RTP端口为N,则RTCP端口为N+1
  • 例如:RTP端口15060,RTCP端口15061

GB28181中的RTCP使用:

  • 平台定期发送RR包,告知设备接收质量
  • 设备可根据丢包率调整码率
  • RTCP不是必须的,很多设备不发送RTCP

5.3 RTP接收器实现

下面实现一个RTP接收器,用于接收GB28181设备推送的PS流。

RTP包解析器:

java 复制代码
package com.video.platform.rtp;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.nio.ByteBuffer;

/**
 * RTP包解析器
 * 
 * 解析RTP包头,提取媒体数据
 */
@Slf4j
public class RtpPacket {
    
    // RTP固定头长度
    private static final int RTP_HEADER_SIZE = 12;
    
    private byte[] rawData;         // 原始数据
    private RtpHeader header;       // RTP头
    private byte[] payload;         // 载荷数据
    
    /**
     * 从字节数组解析RTP包
     */
    public static RtpPacket parse(byte[] data) {
        if (data == null || data.length < RTP_HEADER_SIZE) {
            throw new IllegalArgumentException("RTP包长度不足");
        }
        
        RtpPacket packet = new RtpPacket();
        packet.rawData = data;
        
        // 解析RTP头
        packet.header = parseHeader(data);
        
        // 提取载荷数据
        int headerLength = RTP_HEADER_SIZE + packet.header.getCsrcCount() * 4;
        if (packet.header.isExtension()) {
            // 跳过扩展头(这里简化处理)
            headerLength += 4;
        }
        
        int payloadLength = data.length - headerLength;
        packet.payload = new byte[payloadLength];
        System.arraycopy(data, headerLength, packet.payload, 0, payloadLength);
        
        return packet;
    }
    
    /**
     * 解析RTP头
     */
    private static RtpHeader parseHeader(byte[] data) {
        ByteBuffer buffer = ByteBuffer.wrap(data);
        
        RtpHeader header = new RtpHeader();
        
        // 第1字节:V(2bit) + P(1bit) + X(1bit) + CC(4bit)
        byte byte0 = buffer.get();
        header.setVersion((byte0 >> 6) & 0x03);
        header.setPadding(((byte0 >> 5) & 0x01) == 1);
        header.setExtension(((byte0 >> 4) & 0x01) == 1);
        header.setCsrcCount(byte0 & 0x0F);
        
        // 第2字节:M(1bit) + PT(7bit)
        byte byte1 = buffer.get();
        header.setMarker(((byte1 >> 7) & 0x01) == 1);
        header.setPayloadType(byte1 & 0x7F);
        
        // 第3-4字节:序列号
        header.setSequenceNumber(buffer.getShort() & 0xFFFF);
        
        // 第5-8字节:时间戳
        header.setTimestamp(buffer.getInt() & 0xFFFFFFFFL);
        
        // 第9-12字节:SSRC
        header.setSsrc(buffer.getInt() & 0xFFFFFFFFL);
        
        return header;
    }
    
    /**
     * 获取RTP头
     */
    public RtpHeader getHeader() {
        return header;
    }
    
    /**
     * 获取载荷数据
     */
    public byte[] getPayload() {
        return payload;
    }
    
    /**
     * 获取原始数据
     */
    public byte[] getRawData() {
        return rawData;
    }
    
    /**
     * RTP头信息
     */
    @Data
    public static class RtpHeader {
        private int version;            // 版本号(2bit)
        private boolean padding;        // 填充标志(1bit)
        private boolean extension;      // 扩展头标志(1bit)
        private int csrcCount;          // CSRC数量(4bit)
        private boolean marker;         // 标记位(1bit)
        private int payloadType;        // 载荷类型(7bit)
        private int sequenceNumber;     // 序列号(16bit)
        private long timestamp;         // 时间戳(32bit)
        private long ssrc;              // SSRC(32bit)
    }
}

RTP接收器:

java 复制代码
package com.video.platform.rtp;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * RTP接收器
 * 
 * 接收RTP包,转发给对应的会话处理器
 */
@Slf4j
@Component
public class RtpReceiver {
    
    // RTP端口范围
    private static final int RTP_PORT_START = 30000;
    private static final int RTP_PORT_END = 40000;
    
    // 当前可用的RTP端口
    private int currentRtpPort = RTP_PORT_START;
    
    // 端口 -> 会话映射
    private Map<Integer, RtpSession> portSessionMap = new ConcurrentHashMap<>();
    
    // RTP接收线程池
    private ExecutorService executorService = Executors.newCachedThreadPool();
    
    /**
     * 分配RTP端口
     * 
     * 为新的视频会话分配可用的RTP端口
     * 
     * @return RTP端口号
     */
    public synchronized int allocatePort() {
        // 循环查找可用端口
        int port = currentRtpPort;
        for (int i = 0; i < (RTP_PORT_END - RTP_PORT_START); i++) {
            if (!portSessionMap.containsKey(port)) {
                currentRtpPort = port + 2;  // RTP端口是偶数,跳过RTCP端口
                if (currentRtpPort >= RTP_PORT_END) {
                    currentRtpPort = RTP_PORT_START;
                }
                return port;
            }
            port += 2;
            if (port >= RTP_PORT_END) {
                port = RTP_PORT_START;
            }
        }
        
        throw new RuntimeException("无可用的RTP端口");
    }
    
    /**
     * 创建RTP会话
     * 
     * @param ssrc 同步源标识
     * @param port RTP端口
     * @param handler RTP包处理器
     * @return RTP会话
     */
    public RtpSession createSession(String ssrc, int port, RtpPacketHandler handler) {
        try {
            // 创建UDP Socket
            DatagramSocket rtpSocket = new DatagramSocket(port);
            DatagramSocket rtcpSocket = new DatagramSocket(port + 1);
            
            log.info("创建RTP会话:SSRC={}, RTP端口={}, RTCP端口={}", 
                ssrc, port, port + 1);
            
            // 创建会话
            RtpSession session = new RtpSession();
            session.setSsrc(ssrc);
            session.setRtpPort(port);
            session.setRtcpPort(port + 1);
            session.setRtpSocket(rtpSocket);
            session.setRtcpSocket(rtcpSocket);
            session.setHandler(handler);
            session.setRunning(true);
            
            // 加入映射
            portSessionMap.put(port, session);
            
            // 启动接收线程
            startReceiving(session);
            
            return session;
            
        } catch (SocketException e) {
            log.error("创建RTP会话失败:port={}", port, e);
            throw new RuntimeException("创建RTP会话失败", e);
        }
    }
    
    /**
     * 启动RTP接收线程
     */
    private void startReceiving(RtpSession session) {
        // RTP接收线程
        executorService.execute(() -> {
            log.info("RTP接收线程启动:port={}", session.getRtpPort());
            
            byte[] buffer = new byte[1500];  // MTU大小
            DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
            
            while (session.isRunning()) {
                try {
                    // 接收RTP包
                    session.getRtpSocket().receive(packet);
                    
                    // 解析RTP包
                    byte[] data = new byte[packet.getLength()];
                    System.arraycopy(packet.getData(), 0, data, 0, packet.getLength());
                    
                    RtpPacket rtpPacket = RtpPacket.parse(data);
                    
                    // 更新统计
                    session.incrementPacketCount();
                    session.updateLastReceiveTime();
                    
                    // 检测丢包
                    int seqNum = rtpPacket.getHeader().getSequenceNumber();
                    if (session.getLastSequenceNumber() != -1) {
                        int expected = (session.getLastSequenceNumber() + 1) & 0xFFFF;
                        if (seqNum != expected) {
                            log.warn("检测到丢包:期望={}, 实际={}, SSRC={}", 
                                expected, seqNum, session.getSsrc());
                        }
                    }
                    session.setLastSequenceNumber(seqNum);
                    
                    // 交给处理器处理
                    if (session.getHandler() != null) {
                        session.getHandler().handleRtpPacket(rtpPacket, session);
                    }
                    
                } catch (IOException e) {
                    if (session.isRunning()) {
                        log.error("接收RTP包异常:port={}", session.getRtpPort(), e);
                    }
                }
            }
            
            log.info("RTP接收线程停止:port={}", session.getRtpPort());
        });
        
        // RTCP接收线程(可选)
        executorService.execute(() -> {
            log.info("RTCP接收线程启动:port={}", session.getRtcpPort());
            
            byte[] buffer = new byte[1500];
            DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
            
            while (session.isRunning()) {
                try {
                    session.getRtcpSocket().receive(packet);
                    log.debug("收到RTCP包:length={}", packet.getLength());
                    // 这里可以解析RTCP包,获取传输质量信息
                    
                } catch (IOException e) {
                    if (session.isRunning()) {
                        log.error("接收RTCP包异常:port={}", session.getRtcpPort(), e);
                    }
                }
            }
            
            log.info("RTCP接收线程停止:port={}", session.getRtcpPort());
        });
    }
    
    /**
     * 关闭RTP会话
     */
    public void closeSession(String ssrc) {
        RtpSession session = portSessionMap.values().stream()
            .filter(s -> s.getSsrc().equals(ssrc))
            .findFirst()
            .orElse(null);
        
        if (session != null) {
            closeSession(session);
        }
    }
    
    /**
     * 关闭RTP会话
     */
    private void closeSession(RtpSession session) {
        log.info("关闭RTP会话:SSRC={}, port={}", session.getSsrc(), session.getRtpPort());
        
        // 停止接收
        session.setRunning(false);
        
        // 关闭Socket
        if (session.getRtpSocket() != null && !session.getRtpSocket().isClosed()) {
            session.getRtpSocket().close();
        }
        if (session.getRtcpSocket() != null && !session.getRtcpSocket().isClosed()) {
            session.getRtcpSocket().close();
        }
        
        // 移除映射
        portSessionMap.remove(session.getRtpPort());
    }
    
    /**
     * RTP包处理器接口
     */
    public interface RtpPacketHandler {
        void handleRtpPacket(RtpPacket packet, RtpSession session);
    }
}

RTP会话:

java 复制代码
package com.video.platform.rtp;

import lombok.Data;

import java.net.DatagramSocket;

/**
 * RTP会话
 * 
 * 表示一个视频流的RTP会话
 */
@Data
public class RtpSession {
    
    private String ssrc;                    // 同步源标识
    private int rtpPort;                    // RTP端口
    private int rtcpPort;                   // RTCP端口
    private DatagramSocket rtpSocket;       // RTP Socket
    private DatagramSocket rtcpSocket;      // RTCP Socket
    
    private RtpReceiver.RtpPacketHandler handler;  // 包处理器
    
    private boolean running;                // 运行标志
    
    // 统计信息
    private long packetCount = 0;           // 接收包数
    private long byteCount = 0;             // 接收字节数
    private long lastReceiveTime;           // 最后接收时间
    private int lastSequenceNumber = -1;    // 最后序列号
    
    /**
     * 增加包计数
     */
    public void incrementPacketCount() {
        packetCount++;
    }
    
    /**
     * 更新最后接收时间
     */
    public void updateLastReceiveTime() {
        lastReceiveTime = System.currentTimeMillis();
    }
    
    /**
     * 判断是否超时(30秒未收到数据)
     */
    public boolean isTimeout() {
        return System.currentTimeMillis() - lastReceiveTime > 30000;
    }
}

6. GB28181设备注册流程

6.1 注册时序图

GB28181设备注册采用SIP REGISTER方法,流程如下:

复制代码
设备端                                 平台端
  │                                      │
  │  ① REGISTER(不带认证)               │
  │ ──────────────────────────────────> │
  │                                      │  检查请求
  │                                      │  需要认证
  │  ② 401 Unauthorized(带WWW-Authenticate)│
  │ <────────────────────────────────── │
  │                                      │
  │  计算Digest认证                       │
  │  response = MD5(...)                │
  │                                      │
  │  ③ REGISTER(带Authorization)        │
  │ ──────────────────────────────────> │
  │                                      │  验证认证信息
  │                                      │  注册成功
  │  ④ 200 OK                            │
  │ <────────────────────────────────── │
  │                                      │
  │  注册成功,进入在线状态                 │
  │                                      │  设备在线
  │                                      │
  │  ⑤ MESSAGE(心跳)                    │
  │ ──────────────────────────────────> │
  │                                      │
  │  ⑥ 200 OK                            │
  │ <────────────────────────────────── │
  │                                      │
  │  定期发送心跳...                      │
  │                                      │

关键步骤:

  1. 第一次REGISTER:设备发送不带认证的注册请求
  2. 401响应:平台要求认证,返回401和nonce(随机数)
  3. 第二次REGISTER:设备计算Digest认证,携带Authorization头
  4. 200响应:平台验证通过,返回200 OK
  5. 心跳保活:设备定期发送MESSAGE心跳

6.2 注册认证机制

GB28181使用HTTP Digest认证(RFC 2617),而不是明文密码。

为什么要用Digest认证?

  • 不传输明文密码,提高安全性
  • 防止重放攻击(nonce是一次性的)
  • 简单易实现,不需要HTTPS

Digest认证计算过程:

平台端(401响应):

复制代码
WWW-Authenticate: Digest realm="平台域", nonce="随机数", algorithm=MD5

设备端(计算response):

复制代码
① A1 = MD5(username:realm:password)
② A2 = MD5(method:uri)
③ response = MD5(A1:nonce:A2)

示例:

java 复制代码
username = "34020000001320000001"
realm = "10.10.10.10"
password = "123456"
method = "REGISTER"
uri = "sip:10.10.10.10:5060"
nonce = "abc123def456"

A1 = MD5("34020000001320000001:10.10.10.10:123456")
   = "e10adc3949ba59abbe56e057f20f883e"

A2 = MD5("REGISTER:sip:10.10.10.10:5060")
   = "f5bb0c8de146c67b44babbf4e6584cc0"

response = MD5("e10adc3949ba59abbe56e057f20f883e:abc123def456:f5bb0c8de146c67b44babbf4e6584cc0")
         = "6629fae49393a05397450978507c4ef1"

设备端发送:

复制代码
Authorization: Digest username="34020000001320000001",
                      realm="10.10.10.10",
                      nonce="abc123def456",
                      uri="sip:10.10.10.10:5060",
                      response="6629fae49393a05397450978507c4ef1",
                      algorithm=MD5

平台端验证:

  • 从数据库查询设备密码
  • 使用相同算法计算response
  • 比对计算结果和设备提交的response
  • 一致则认证通过

6.3 注册流程实现

Digest认证工具类:

java 复制代码
package com.video.platform.sip.auth;

import lombok.extern.slf4j.Slf4j;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.UUID;

/**
 * Digest认证工具类
 * 
 * 实现HTTP Digest认证算法(RFC 2617)
 */
@Slf4j
public class DigestAuthUtil {
    
    /**
     * 生成nonce(随机数)
     * 用于防止重放攻击
     */
    public static String generateNonce() {
        return UUID.randomUUID().toString().replace("-", "");
    }
    
    /**
     * 计算Digest认证的response
     * 
     * @param username 用户名
     * @param realm 域
     * @param password 密码
     * @param method 请求方法(REGISTER/INVITE等)
     * @param uri 请求URI
     * @param nonce 随机数
     * @return response字符串
     */
    public static String calculateResponse(
        String username, 
        String realm, 
        String password, 
        String method, 
        String uri, 
        String nonce
    ) {
        // A1 = MD5(username:realm:password)
        String a1 = md5(username + ":" + realm + ":" + password);
        
        // A2 = MD5(method:uri)
        String a2 = md5(method + ":" + uri);
        
        // response = MD5(A1:nonce:A2)
        String response = md5(a1 + ":" + nonce + ":" + a2);
        
        log.debug("Digest认证计算:");
        log.debug("  username={}, realm={}", username, realm);
        log.debug("  method={}, uri={}, nonce={}", method, uri, nonce);
        log.debug("  A1={}", a1);
        log.debug("  A2={}", a2);
        log.debug("  response={}", response);
        
        return response;
    }
    
    /**
     * 验证Digest认证
     * 
     * @param username 用户名
     * @param realm 域
     * @param password 密码(从数据库查询)
     * @param method 请求方法
     * @param uri 请求URI
     * @param nonce 随机数
     * @param clientResponse 客户端提交的response
     * @return 是否验证通过
     */
    public static boolean verifyResponse(
        String username, 
        String realm, 
        String password, 
        String method, 
        String uri, 
        String nonce, 
        String clientResponse
    ) {
        // 计算期望的response
        String expectedResponse = calculateResponse(username, realm, password, method, uri, nonce);
        
        // 比对
        boolean result = expectedResponse.equals(clientResponse);
        
        log.info("Digest认证验证:username={}, result={}", username, result);
        
        return result;
    }
    
    /**
     * MD5哈希
     */
    private static String md5(String input) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] bytes = md.digest(input.getBytes(StandardCharsets.UTF_8));
            
            StringBuilder sb = new StringBuilder();
            for (byte b : bytes) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
            
        } catch (Exception e) {
            throw new RuntimeException("MD5计算失败", e);
        }
    }
    
    /**
     * 解析Authorization头
     * 
     * @param authorization Authorization头的值
     * @return 解析结果
     */
    public static AuthorizationInfo parseAuthorization(String authorization) {
        if (authorization == null || !authorization.startsWith("Digest ")) {
            return null;
        }
        
        AuthorizationInfo info = new AuthorizationInfo();
        
        // 去掉"Digest "前缀
        String params = authorization.substring(7);
        
        // 解析参数
        String[] pairs = params.split(",");
        for (String pair : pairs) {
            String[] kv = pair.trim().split("=", 2);
            if (kv.length == 2) {
                String key = kv[0].trim();
                String value = kv[1].trim().replaceAll("\"", "");
                
                switch (key) {
                    case "username":
                        info.setUsername(value);
                        break;
                    case "realm":
                        info.setRealm(value);
                        break;
                    case "nonce":
                        info.setNonce(value);
                        break;
                    case "uri":
                        info.setUri(value);
                        break;
                    case "response":
                        info.setResponse(value);
                        break;
                    case "algorithm":
                        info.setAlgorithm(value);
                        break;
                }
            }
        }
        
        return info;
    }
    
    /**
     * Authorization信息
     */
    public static class AuthorizationInfo {
        private String username;
        private String realm;
        private String nonce;
        private String uri;
        private String response;
        private String algorithm;
        
        // Getter和Setter省略
        public String getUsername() { return username; }
        public void setUsername(String username) { this.username = username; }
        public String getRealm() { return realm; }
        public void setRealm(String realm) { this.realm = realm; }
        public String getNonce() { return nonce; }
        public void setNonce(String nonce) { this.nonce = nonce; }
        public String getUri() { return uri; }
        public void setUri(String uri) { this.uri = uri; }
        public String getResponse() { return response; }
        public void setResponse(String response) { this.response = response; }
        public String getAlgorithm() { return algorithm; }
        public void setAlgorithm(String algorithm) { this.algorithm = algorithm; }
    }
}

REGISTER请求处理器:

java 复制代码
package com.video.platform.sip.handler;

import com.video.platform.config.SipConfig;
import com.video.platform.sip.SipServer;
import com.video.platform.sip.auth.DigestAuthUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.sip.*;
import javax.sip.address.Address;
import javax.sip.header.*;
import javax.sip.message.Request;
import javax.sip.message.Response;
import java.text.ParseException;

/**
 * REGISTER请求处理器
 * 
 * 处理设备注册请求,实现Digest认证
 */
@Slf4j
@Component
public class RegisterRequestHandler {
    
    @Autowired
    private SipConfig sipConfig;
    
    @Autowired
    private SipServer sipServer;
    
    /**
     * 处理REGISTER请求
     */
    public void handleRegister(RequestEvent requestEvent) {
        try {
            Request request = requestEvent.getRequest();
            
            // 提取设备ID
            FromHeader fromHeader = (FromHeader) request.getHeader(FromHeader.NAME);
            Address fromAddress = fromHeader.getAddress();
            String deviceId = fromAddress.getURI().toString();
            // 提取纯设备ID:sip:34020000001320000001@10.10.10.20 -> 34020000001320000001
            if (deviceId.contains("@")) {
                deviceId = deviceId.substring(deviceId.indexOf(":") + 1, deviceId.indexOf("@"));
            }
            
            log.info("收到REGISTER请求:设备ID={}", deviceId);
            
            // 检查Authorization头
            AuthorizationHeader authHeader = (AuthorizationHeader) request.getHeader(AuthorizationHeader.NAME);
            
            if (authHeader == null) {
                // 第一次注册,没有认证信息,返回401要求认证
                send401Response(requestEvent, deviceId);
            } else {
                // 第二次注册,验证认证信息
                verifyAndRegister(requestEvent, deviceId, authHeader);
            }
            
        } catch (Exception e) {
            log.error("处理REGISTER请求异常", e);
            send500Response(requestEvent);
        }
    }
    
    /**
     * 发送401响应,要求认证
     */
    private void send401Response(RequestEvent requestEvent, String deviceId) 
        throws ParseException, InvalidArgumentException, SipException {
        
        log.info("发送401响应,要求设备认证:{}", deviceId);
        
        // 生成nonce
        String nonce = DigestAuthUtil.generateNonce();
        
        // 创建401响应
        Response response = sipServer.getMessageFactory().createResponse(
            Response.UNAUTHORIZED, 
            requestEvent.getRequest()
        );
        
        // 添加WWW-Authenticate头
        WWWAuthenticateHeader wwwAuthHeader = sipServer.getHeaderFactory().createWWWAuthenticateHeader("Digest");
        wwwAuthHeader.setParameter("realm", sipConfig.getDomain());
        wwwAuthHeader.setParameter("nonce", nonce);
        wwwAuthHeader.setParameter("algorithm", "MD5");
        
        response.addHeader(wwwAuthHeader);
        
        // 发送响应
        ServerTransaction serverTransaction = requestEvent.getServerTransaction();
        if (serverTransaction == null) {
            serverTransaction = sipServer.getSipProvider().getNewServerTransaction(requestEvent.getRequest());
        }
        serverTransaction.sendResponse(response);
        
        log.info("401响应已发送:nonce={}", nonce);
    }
    
    /**
     * 验证认证信息并注册
     */
    private void verifyAndRegister(RequestEvent requestEvent, String deviceId, AuthorizationHeader authHeader) 
        throws Exception {
        
        // 解析Authorization头
        DigestAuthUtil.AuthorizationInfo authInfo = DigestAuthUtil.parseAuthorization(
            authHeader.toString().substring("Authorization: ".length())
        );
        
        log.info("验证设备认证:设备ID={}, username={}", deviceId, authInfo.getUsername());
        
        // 从数据库查询设备密码(这里简化为配置中的密码)
        String password = sipConfig.getPassword();
        
        // 验证认证
        boolean verified = DigestAuthUtil.verifyResponse(
            authInfo.getUsername(),
            authInfo.getRealm(),
            password,
            "REGISTER",
            authInfo.getUri(),
            authInfo.getNonce(),
            authInfo.getResponse()
        );
        
        if (verified) {
            // 认证通过,发送200 OK
            send200Response(requestEvent, deviceId);
            
            // 更新设备状态为在线
            updateDeviceStatus(deviceId, true);
            
        } else {
            // 认证失败,发送403 Forbidden
            log.warn("设备认证失败:{}", deviceId);
            send403Response(requestEvent);
        }
    }
    
    /**
     * 发送200 OK响应
     */
    private void send200Response(RequestEvent requestEvent, String deviceId) 
        throws ParseException, InvalidArgumentException, SipException {
        
        log.info("发送200 OK响应,注册成功:{}", deviceId);
        
        // 创建200响应
        Response response = sipServer.getMessageFactory().createResponse(
            Response.OK, 
            requestEvent.getRequest()
        );
        
        // 添加Expires头
        ExpiresHeader expiresHeader = sipServer.getHeaderFactory().createExpiresHeader(
            sipConfig.getRegisterExpires()
        );
        response.addHeader(expiresHeader);
        
        // 添加Date头
        DateHeader dateHeader = sipServer.getHeaderFactory().createDateHeader(
            new java.util.GregorianCalendar()
        );
        response.addHeader(dateHeader);
        
        // 发送响应
        ServerTransaction serverTransaction = requestEvent.getServerTransaction();
        if (serverTransaction == null) {
            serverTransaction = sipServer.getSipProvider().getNewServerTransaction(requestEvent.getRequest());
        }
        serverTransaction.sendResponse(response);
        
        log.info("200 OK响应已发送,设备注册成功:{}", deviceId);
    }
    
    /**
     * 发送403 Forbidden响应
     */
    private void send403Response(RequestEvent requestEvent) 
        throws ParseException, InvalidArgumentException, SipException {
        
        Response response = sipServer.getMessageFactory().createResponse(
            Response.FORBIDDEN, 
            requestEvent.getRequest()
        );
        
        ServerTransaction serverTransaction = requestEvent.getServerTransaction();
        if (serverTransaction == null) {
            serverTransaction = sipServer.getSipProvider().getNewServerTransaction(requestEvent.getRequest());
        }
        serverTransaction.sendResponse(response);
    }
    
    /**
     * 发送500响应
     */
    private void send500Response(RequestEvent requestEvent) {
        try {
            Response response = sipServer.getMessageFactory().createResponse(
                Response.SERVER_INTERNAL_ERROR, 
                requestEvent.getRequest()
            );
            
            ServerTransaction serverTransaction = requestEvent.getServerTransaction();
            if (serverTransaction == null) {
                serverTransaction = sipServer.getSipProvider().getNewServerTransaction(requestEvent.getRequest());
            }
            serverTransaction.sendResponse(response);
            
        } catch (Exception e) {
            log.error("发送500响应失败", e);
        }
    }
    
    /**
     * 更新设备状态
     */
    private void updateDeviceStatus(String deviceId, boolean online) {
        // 这里应该更新数据库中的设备状态
        // 简化实现,只打印日志
        log.info("更新设备状态:设备ID={}, 在线={}", deviceId, online);
        
        // 实际项目中:
        // deviceService.updateDeviceStatus(deviceId, online ? "在线" : "离线");
    }
}

7. 常见坑点与最佳实践

7.1 常见坑点

坑点1:IP地址配置错误

java 复制代码
// ❌ 错误:使用127.0.0.1
sipConfig.setIp("127.0.0.1");  // 设备无法连接

// ✅ 正确:使用服务器实际IP
sipConfig.setIp("10.10.10.10");  // 设备可以连接

为什么会有这个坑?

  • 设备在外网,无法访问127.0.0.1
  • SIP消息中的IP地址会被设备使用
  • Via、Contact等头部都包含IP地址

坑点2:端口被占用

java 复制代码
// 检查端口是否可用
private boolean isPortAvailable(int port) {
    try (ServerSocket serverSocket = new ServerSocket(port)) {
        return true;
    } catch (IOException e) {
        return false;
    }
}

坑点3:RTP端口耗尽

java 复制代码
// ❌ 错误:没有释放端口
public void stopVideo(String ssrc) {
    rtpSession.close();  // 只关闭会话,没有回收端口
}

// ✅ 正确:及时回收端口
public void stopVideo(String ssrc) {
    rtpSession.close();
    rtpReceiver.releasePort(rtpSession.getPort());  // 回收端口
}

坑点4:PS流解析失败

java 复制代码
// GB28181强制使用PS流封装
// 需要解析PS头,提取H.264/H.265裸流
// 很多开发者不知道PS是什么,导致解析失败

坑点5:时间戳计算错误

java 复制代码
// ❌ 错误:使用系统时间
rtpPacket.setTimestamp(System.currentTimeMillis());

// ✅ 正确:使用相对时间戳(以时钟频率为单位)
// 假设时钟频率90000Hz,帧率30fps
long timestamp = frameIndex * (90000 / 30);
rtpPacket.setTimestamp(timestamp);

7.2 最佳实践

实践1:统一异常处理

java 复制代码
@ControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(SipException.class)
    public ResponseEntity<String> handleSipException(SipException e) {
        log.error("SIP异常", e);
        return ResponseEntity.status(500).body("SIP协议错误");
    }
}

实践2:连接池管理

java 复制代码
// RTP端口池
@Component
public class RtpPortPool {
    
    private Queue<Integer> availablePorts = new ConcurrentLinkedQueue<>();
    
    @PostConstruct
    public void init() {
        // 初始化端口池(30000-40000)
        for (int port = 30000; port < 40000; port += 2) {
            availablePorts.offer(port);
        }
    }
    
    public int borrowPort() {
        Integer port = availablePorts.poll();
        if (port == null) {
            throw new RuntimeException("端口池耗尽");
        }
        return port;
    }
    
    public void returnPort(int port) {
        availablePorts.offer(port);
    }
}

实践3:超时检测

java 复制代码
// 定时检测会话超时
@Scheduled(fixedRate = 10000)  // 每10秒检测一次
public void checkTimeout() {
    List<RtpSession> timeoutSessions = sessionManager.getAllSessions().stream()
        .filter(RtpSession::isTimeout)
        .collect(Collectors.toList());
    
    for (RtpSession session : timeoutSessions) {
        log.warn("会话超时,自动关闭:SSRC={}", session.getSsrc());
        sessionManager.closeSession(session.getSsrc());
    }
}

实践4:配置外部化

yaml 复制代码
# application.yml
sip:
  server:
    ip: 10.10.10.10
    port: 5060
    domain: 10.10.10.10
    server-id: 34020000002000000001
    transport: UDP
    username: admin
    password: 123456
    keepalive-interval: 60
    keepalive-timeout: 180
    register-expires: 3600

实践5:日志记录

java 复制代码
// 记录关键操作
log.info("设备注册:设备ID={}, IP={}, 端口={}", deviceId, ip, port);
log.info("视频点播:设备ID={}, 通道ID={}, RTP端口={}", deviceId, channelId, rtpPort);
log.warn("心跳超时:设备ID={}, 最后心跳={}", deviceId, lastHeartbeat);
log.error("RTP接收异常:SSRC={}, 异常={}", ssrc, e.getMessage());

8. 面试高频题

Q1: GB28181协议是什么?为什么要用它?

参考答案:

GB28181(公共安全视频监控联网系统信息传输、交换、控制技术要求)是国家强制标准,用于视频监控设备的统一接入。

为什么要用GB28181:

  1. 统一标准

    • 不同厂商设备使用相同协议
    • 一套代码对接所有设备
    • 大幅降低开发和维护成本
  2. 跨平台兼容

    • 基于SIP/RTP等互联网标准协议
    • 支持多种编程语言实现
    • Web端可通过WebRTC播放
  3. 国家要求

    • 公安、交通等行业强制使用
    • 政府项目必须支持
  4. 扩展性强

    • 支持设备注册、心跳、目录查询
    • 支持实时视频、历史回放、云台控制
    • 支持级联(上下级平台互联)

相比厂商SDK的优势:

  • SDK通常很重,GB28181轻量级
  • SDK绑定平台,GB28181通用
  • SDK难以跨平台,GB28181易于实现
  • SDK维护成本高,GB28181标准稳定

Q2: GB28181协议分为哪几层?各层的作用是什么?

参考答案:

GB28181协议采用分层架构,从下到上分为:

1. 网络层(TCP/UDP)

  • 提供基础的网络传输能力
  • GB28181通常使用UDP传输

2. 媒体传输层(RTP/RTCP)

  • RTP:传输音视频数据
  • RTCP:监控传输质量,提供反馈
  • 包含时间戳、序列号、载荷类型等

3. 会话描述层(SDP)

  • 描述媒体参数(编码格式、传输地址)
  • 用于双方协商媒体能力
  • 在SIP消息体中传输

4. 信令控制层(SIP)

  • 处理设备注册、心跳保活
  • 发起视频点播、历史回放
  • 控制会话的建立、修改、终止

5. 应用层

  • 目录查询、云台控制
  • 报警订阅、录像检索
  • 业务功能实现

分层的好处:

  • 职责分离,易于维护
  • 可扩展,可替换底层协议
  • 易于定位问题

Q3: SIP协议在GB28181中起什么作用?请说明REGISTER的完整流程。

参考答案:

SIP的作用:

SIP(Session Initiation Protocol)会话发起协议,在GB28181中用于:

  1. 设备注册(REGISTER)
  2. 会话建立(INVITE)
  3. 心跳保活(MESSAGE)
  4. 会话结束(BYE)
  5. 目录查询(MESSAGE)

REGISTER注册流程:

复制代码
1. 设备发送REGISTER请求(不带认证)
   ↓
2. 平台返回401 Unauthorized + WWW-Authenticate头
   (包含realm、nonce、algorithm)
   ↓
3. 设备计算Digest认证:
   A1 = MD5(username:realm:password)
   A2 = MD5(method:uri)
   response = MD5(A1:nonce:A2)
   ↓
4. 设备发送REGISTER请求(带Authorization头)
   ↓
5. 平台验证认证信息:
   - 从数据库查询密码
   - 使用相同算法计算response
   - 比对结果
   ↓
6. 验证通过,返回200 OK
   设备进入在线状态

为什么要用Digest认证?

  • 不传输明文密码
  • 防止重放攻击(nonce一次性)
  • 简单易实现

Q4: RTP协议的作用是什么?RTP包头包含哪些重要字段?

参考答案:

RTP的作用:

RTP(Real-time Transport Protocol)实时传输协议,专为音视频传输设计,在UDP基础上提供:

  1. 时间戳:音视频同步
  2. 序列号:检测丢包和乱序
  3. 载荷类型:标识编码格式
  4. 同步源标识:区分不同媒体流

RTP包头重要字段:

字段 位数 作用
序列号 16bit 检测丢包、乱序,每发送一个包+1
时间戳 32bit 音视频同步,表示采样时刻
载荷类型 7bit 标识编码格式(96=PS, 98=H.264)
SSRC 32bit 唯一标识一个RTP流
Marker 1bit 标识帧结束(视频)或通话开始(音频)

示例:

java 复制代码
// 解析RTP包头
int version = (byte0 >> 6) & 0x03;        // 版本号
boolean marker = ((byte1 >> 7) & 0x01) == 1;  // Marker位
int payloadType = byte1 & 0x7F;          // 载荷类型
int sequenceNumber = buffer.getShort();   // 序列号
long timestamp = buffer.getInt();         // 时间戳
long ssrc = buffer.getInt();              // SSRC

时间戳计算:

复制代码
时钟频率:90000Hz
帧率:30fps
每帧时间戳增量 = 90000 / 30 = 3000

第1帧:timestamp = 0
第2帧:timestamp = 3000
第3帧:timestamp = 6000

Q5: SDP协议的作用是什么?如何解析SDP消息?

参考答案:

SDP的作用:

SDP(Session Description Protocol)会话描述协议,用于描述多媒体会话的参数,让双方能够:

  1. 协商编码格式(H.264/H.265/AAC)
  2. 确定传输地址(IP和端口)
  3. 声明媒体属性(码率、分辨率)

典型SDP示例:

复制代码
v=0                                   ← 版本号
o=user 0 0 IN IP4 10.10.10.20        ← 会话所有者
s=Play                                ← 会话名称
c=IN IP4 10.10.10.20                 ← 连接地址
t=0 0                                 ← 时间描述
m=video 15060 RTP/AVP 96 98          ← 媒体描述
a=rtpmap:96 PS/90000                 ← 载荷96:PS流
a=rtpmap:98 H264/90000               ← 载荷98:H.264
a=recvonly                            ← 接收方向
y=0100000001                          ← SSRC

关键字段:

  • m=:媒体类型、端口、协议、载荷类型
  • a=rtpmap:载荷类型到编码格式的映射
  • c=:连接地址
  • a=recvonly/sendonly/sendrecv:传输方向

解析实现:

java 复制代码
public static SdpInfo parse(String sdpContent) {
    String[] lines = sdpContent.split("\r\n|\n");
    SdpInfo sdpInfo = new SdpInfo();
    
    for (String line : lines) {
        char type = line.charAt(0);
        String value = line.substring(2);
        
        switch (type) {
            case 'm':  // 媒体描述
                MediaInfo media = parseMedia(value);
                sdpInfo.addMedia(media);
                break;
            case 'a':  // 属性
                if (value.startsWith("rtpmap:")) {
                    parseRtpmap(value, currentMedia);
                }
                break;
            // ... 其他字段
        }
    }
    
    return sdpInfo;
}

Q6: GB28181中的PS流是什么?为什么要用PS流?

参考答案:

PS流(Program Stream):

  • MPEG-2标准定义的一种封装格式
  • 可以包含多路音视频流
  • 适合录像回放和长时间传输
  • GB28181强制使用PS流封装

为什么要用PS流?

  1. 统一标准

    • 避免厂商各自定义封装格式
    • 确保互操作性
  2. 多路复用

    • 一个PS流可以包含音频+视频
    • 适合录像回放
  3. 时间戳同步

    • PS头包含时间戳信息
    • 保证音视频同步
  4. 成熟稳定

    • MPEG-2是成熟标准
    • 有大量开源库支持

PS流处理:

java 复制代码
// 接收RTP包(载荷是PS流)
byte[] psData = rtpPacket.getPayload();

// 解析PS头
PsHeader psHeader = PsParser.parseHeader(psData);

// 提取H.264/H.265裸流
byte[] h264Data = PsParser.extractH264(psData);

// 转发给流媒体服务器
mediaServer.pushH264Stream(h264Data);

常见坑点:

  • 很多开发者不知道PS是什么
  • 直接把PS流当H.264推流,导致播放失败
  • 需要解析PS头,提取裸流

Q7: 如何实现GB28181的心跳保活机制?

参考答案:

心跳保活机制:

设备定期向平台发送MESSAGE心跳消息,平台检测心跳超时,标记设备离线。

实现要点:

1. 设备端发送心跳:

java 复制代码
// 每60秒发送一次心跳
@Scheduled(fixedRate = 60000)
public void sendKeepalive() {
    String keepaliveXml = "<?xml version=\"1.0\"?>" +
        "<Notify>" +
        "<CmdType>Keepalive</CmdType>" +
        "<SN>12345</SN>" +
        "<DeviceID>34020000001320000001</DeviceID>" +
        "<Status>OK</Status>" +
        "</Notify>";
    
    sipClient.sendMessage(keepaliveXml);
}

2. 平台端接收心跳:

java 复制代码
@Override
public void handleMessage(RequestEvent requestEvent) {
    Request request = requestEvent.getRequest();
    String body = new String(request.getRawContent());
    
    // 解析XML
    if (body.contains("<CmdType>Keepalive</CmdType>")) {
        String deviceId = extractDeviceId(body);
        
        // 更新最后心跳时间
        deviceService.updateLastKeepalive(deviceId, new Date());
        
        // 返回200 OK
        send200Response(requestEvent);
    }
}

3. 定时检测超时:

java 复制代码
// 每30秒检测一次
@Scheduled(fixedRate = 30000)
public void checkKeepaliveTimeout() {
    // 查询超过180秒未心跳的设备
    List<Device> timeoutDevices = deviceService.selectTimeoutDevices(180);
    
    for (Device device : timeoutDevices) {
        log.warn("设备心跳超时:{}", device.getDeviceId());
        
        // 标记离线
        deviceService.updateStatus(device.getDeviceId(), "离线");
        
        // 关闭相关会话
        sessionManager.closeSessionsByDevice(device.getDeviceId());
    }
}

4. 心跳恢复:

java 复制代码
// 设备重新发送心跳
public void handleKeepalive(String deviceId) {
    Device device = deviceService.getById(deviceId);
    
    if ("离线".equals(device.getStatus())) {
        log.info("设备心跳恢复:{}", deviceId);
        deviceService.updateStatus(deviceId, "在线");
    }
    
    // 更新心跳时间
    deviceService.updateLastKeepalive(deviceId, new Date());
}

配置建议:

  • 心跳间隔:60秒
  • 超时时间:180秒(3倍心跳间隔)
  • 检测周期:30秒

Q8: GB28181如何实现视频点播?说明INVITE流程。

参考答案:

视频点播流程:

复制代码
平台                                   设备
  │                                      │
  │  ① INVITE(带SDP)                   │
  │ ──────────────────────────────────> │
  │                                      │  解析SDP
  │                                      │  准备推流
  │  ② 200 OK(带SDP)                   │
  │ <────────────────────────────────── │
  │                                      │
  │  解析SDP,获取设备推流地址             │
  │                                      │
  │  ③ ACK                               │
  │ ──────────────────────────────────> │
  │                                      │
  │  ④ RTP媒体流                         │
  │ <════════════════════════════════  │
  │                                      │
  │  播放视频...                         │
  │                                      │
  │  ⑤ BYE(停止播放)                   │
  │ ──────────────────────────────────> │
  │                                      │  停止推流
  │  ⑥ 200 OK                            │
  │ <────────────────────────────────── │
  │                                      │

实现代码:

1. 平台发送INVITE:

java 复制代码
public void startVideo(String deviceId, String channelId) {
    // 分配RTP端口
    int rtpPort = rtpReceiver.allocatePort();
    
    // 构建平台SDP
    String sdp = buildInviteSdp(rtpPort);
    
    // 发送INVITE请求
    Request invite = buildInviteRequest(deviceId, channelId, sdp);
    sipProvider.sendRequest(invite);
    
    // 创建RTP会话
    String ssrc = generateSsrc(channelId);
    rtpReceiver.createSession(ssrc, rtpPort, rtpPacket -> {
        // 处理RTP包,转发给流媒体服务器
        handleRtpPacket(rtpPacket);
    });
}

private String buildInviteSdp(int rtpPort) {
    return "v=0\r\n" +
        "o=" + sipConfig.getServerId() + " 0 0 IN IP4 " + sipConfig.getIp() + "\r\n" +
        "s=Play\r\n" +
        "c=IN IP4 " + sipConfig.getIp() + "\r\n" +
        "t=0 0\r\n" +
        "m=video " + rtpPort + " RTP/AVP 96 98\r\n" +
        "a=rtpmap:96 PS/90000\r\n" +
        "a=rtpmap:98 H264/90000\r\n" +
        "a=recvonly\r\n";
}

2. 设备返回200 OK:

java 复制代码
// 设备解析INVITE中的SDP,获取平台RTP端口
SdpInfo platformSdp = SdpParser.parse(inviteBody);
String platformIp = platformSdp.getConnectionAddress();
int platformRtpPort = platformSdp.getVideoMedia().getPort();

// 构建设备SDP
String deviceSdp = "v=0\r\n" +
    "o=" + deviceId + " 0 0 IN IP4 " + deviceIp + "\r\n" +
    "s=Play\r\n" +
    "c=IN IP4 " + deviceIp + "\r\n" +
    "t=0 0\r\n" +
    "m=video 15060 RTP/AVP 96\r\n" +  // 设备推流端口
    "a=rtpmap:96 PS/90000\r\n" +
    "a=sendonly\r\n" +
    "y=" + ssrc + "\r\n";

// 发送200 OK
Response ok = createResponse(200, invite, deviceSdp);
sendResponse(ok);

// 开始推流到平台
startPushStream(platformIp, platformRtpPort);

3. 平台发送ACK确认:

java 复制代码
// 收到200 OK后,发送ACK
Request ack = dialog.createAck(cseq);
dialog.sendAck(ack);

// 开始接收RTP流
log.info("开始接收视频流:SSRC={}, 端口={}", ssrc, rtpPort);

4. 平台停止播放:

java 复制代码
public void stopVideo(String ssrc) {
    // 发送BYE请求
    Request bye = dialog.createRequest(Request.BYE);
    dialog.sendRequest(bye);
    
    // 关闭RTP会话
    rtpReceiver.closeSession(ssrc);
    
    log.info("停止视频播放:SSRC={}", ssrc);
}

下一篇预告: 《GB28181设备注册实现》- 深入设备注册流程,掌握设备管理、状态维护、批量注册等核心技术。

相关推荐
FakeOccupational2 小时前
【电路笔记 PCB】Altium Designer : AD使用教程+Altium Designer常见AD操作命令与流程
开发语言·笔记
Coder_Boy_2 小时前
基于SpringAI的在线考试系统-考试系统DDD(领域驱动设计)实现步骤详解
java·数据库·人工智能·spring boot
毕设源码-钟学长2 小时前
【开题答辩全过程】以 基于Java的运动器材销售网站为例,包含答辩的问题和答案
java·开发语言
Miketutu2 小时前
Flutter学习 - 组件通信与网络请求Dio
开发语言·前端·javascript
workflower2 小时前
软件需求规约的质量属性
java·开发语言·数据库·测试用例·需求分析·结对编程
鸣弦artha2 小时前
Flutter框架跨平台鸿蒙开发——Build流程深度解析
开发语言·javascript·flutter
TracyCoder1232 小时前
Java String:从内存模型到不可变设计
java·算法·string
想用offer打牌3 小时前
Spring AI Alibaba与 Agent Scope到底选哪个?
java·人工智能·spring
情缘晓梦.3 小时前
C++ 内存管理
开发语言·jvm·c++