# RtspPublisher
**Repository Path**: AndroidCoderPeng/RtspPublisher
## Basic Information
- **Project Name**: RtspPublisher
- **Description**: Android 端 内置 RTSP 服务器 的低延迟实时推流方案:手机/平板用 Camera2 采集画面,经 OpenGL 处理(水印/滤镜)后由 MediaCodec 硬编码为 H.264,再通过 JNI 交给内嵌的 live555 RTSP 服务对外发布
- **Primary Language**: Unknown
- **License**: MulanPSL-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-09-03
- **Last Updated**: 2026-09-04
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# RtspPublisher
Android 端 **内置 RTSP 服务器** 的低延迟实时推流方案:手机/平板用 Camera2 采集画面,经 OpenGL
处理(水印/滤镜)后由 MediaCodec 硬编码为 H.264,再通过 JNI 交给内嵌的 live555 RTSP 服务对外发布。
与「推流到外部服务器」不同,本项目是 **设备即服务端**:Android 设备自身监听 RTSP 端口(固定 `8554`
),同一局域网内的 VLC / ffplay / 其他播放器可直接拉流,无需任何中转服务器。
```
采集:Camera2 → SurfaceTexture(OES)
处理:OpenGL ES 滤镜管线(水印 / 时间 / 旋转)
编码:MediaCodec 硬编码 H.264(COLOR_FormatSurface,零拷贝送帧)
传输:JNI → live555 RTSPServer → RTP/RTCP → 播放器
```
---
## 特性
- **设备即服务端**:内置 live555 `RTSPServer`,无需搭建流媒体服务器,局域网直连即可播放。
- **硬编码**:MediaCodec `COLOR_FormatSurface` 输入,全程 Surface 通路,避免 CPU 拷贝 YUV。
- **OpenGL 处理管线**:相机帧先经 GL 渲染再送入编码器,可挂载任意 `EglRenderFilter`
(水印、时间、贴纸、美颜、旋转等);内置 `PassThroughFilter` 直通滤镜与 `WatermarkFilter` 水印滤镜。
- **格式自适应**:JNI 层自动识别 AVCC / AnnexB,统一归一化为 AnnexB 后再交给 live555。
- **自动解析 SPS/PPS**:首帧提取 SPS/PPS 用于生成 SDP `sprop-parameter-sets`;编码参数变化时等待下一个
IDR 再重建会话,避免花屏。
- **懒启动**:`initRtsp()` 只记录参数,**不立刻建端口**;服务端在收到首个同时携带 SPS/PPS 与 slice
的帧后才真正拉起,避免空端口占用。
- **多客户端并发**:有界环形帧队列 + per-consumer 消费游标,每个 `FramedSource` 独立记录读取进度,互不影响。
- **新客户端秒开**:`LiveFramedSource` 构造时即调用 `on_client_attach()`
,把游标定位到缓存中最近的关键帧起播;缓存无关键帧时回退到最新一帧。
- **PTS 归一化**:微秒时间戳先减去基准 PTS 再折算到 90 kHz RTP 时间基,并强制单调递增。
- **线程安全**:编码线程 / JNI 线程 / live555 事件循环线程通过互斥锁与原子标志隔离;`stop()` 通过
`triggerEvent` 唤醒阻塞中的事件循环,退出干净。所有 Java 回调都在释放 `_rtsp_mutex` 之后发出,避免回调重入造成死锁。
- **状态回调**:`RtspStatusCallback` 把初始化成功(含可播放 URL)、首个客户端起播、初始化失败、释放等状态透传到
Java 层。
---
## 架构
```mermaid
graph TB
subgraph Java/编码线程
CAM[Camera2 采集] --> GL[EglRenderLayer
OES 纹理 + 滤镜/水印]
GL --> PRE[TextureView 预览]
GL --> CS[MediaCodec InputSurface]
CS --> MC[MediaCodec 硬编码 H.264]
MC --> CB[H264DataCallback
零拷贝 ByteBuffer]
CB --> JNI[JNI pushVideoFrameBuffer]
end
JNI --> RM[RtspManager::dispatch_frame]
RM --> SPLIT{h264_splitter
detect_format}
SPLIT -->|AVCC| CV[avcc_to_annex_b 归一化为 AnnexB]
SPLIT -->|AnnexB| DIRECT[直接使用]
SPLIT -->|Unknown| DROP[丢弃并告警]
CV --> SPS[update_codec_config
extract_sps / extract_pps]
DIRECT --> SPS
SPS --> SLICE{contains_slice ?}
SLICE -->|否| DROP2[纯参数集不入队]
SLICE -->|是| BOOT{_rtsp_started ?}
BOOT -->|否 + 已有 SPS/PPS| SRV[start_rtsp_server]
BOOT -->|是| FQ
SRV -->|成功| OK[回调 INIT_SUCCESS
message = rtsp://ip:8554/live]
SRV -->|失败| FAIL[回调 INIT_FAILED]
SRV --> FQ[(FrameQueue
有界环形缓冲 30 帧
per-consumer 游标)]
subgraph live555 事件循环线程
SVR[RTSPServer :8554] --> SMS[ServerMediaSession /live]
SMS --> SUB[LiveServerMediaSession
OnDemandServerMediaSubsession]
SUB -->|首客户端 startStream| STARTED[回调 StreamStarted
]
SUB --> SRC[LiveFramedSource
5ms 轮询取帧]
SRC --> FR[H264VideoStreamDiscreteFramer]
FR --> SINK[H264VideoRTPSink
SDP 带 sprop-parameter-sets]
SINK --> CLIENT[VLC / ffplay 拉流]
end
FQ -->|事件循环线程 pop| SRC
STOP[release / watchVariable
+ triggerEvent 唤醒] --> SVR
```
### 线程模型
| 线程 | 职责 |
|---------------------------------------------------|-----------------------------------------------------------------------------|
| `CodecWorker`(HandlerThread,`VideoEncoder` 创建) | 相机开关、CaptureSession 构建、MediaCodec 配置/启停 |
| `CodecDrainThread` | `dequeueOutputBuffer` 循环取编码输出,经 `H264DataCallback` 上抛(随 `startEncode()` 新建) |
| `RenderThread`(HandlerThread,`EglRenderLayer` 创建) | OES 纹理采样、滤镜/水印绘制、渲染到预览与编码 Surface |
| 调用 `pushVideoFrameBuffer` 的线程(通常是 drain 线程) | 数据归一化、SPS/PPS 维护、入帧队列 |
| live555 事件循环线程(`LiveRtspServer` 创建) | `doEventLoop`,由 `LiveFramedSource` 主动取帧并 RTP 打包发送 |
`pushVideoFrameBuffer` 与 `RtspManager` 之间的并发由 `_rtsp_mutex` 串行化;
live555 事件循环线程只通过 `FrameQueue` 与其交互,不直接触碰 `RtspManager` 状态。
---
## 目录结构
```
RtspPublisher
├── app/ # 示例 App(权限 + 预览 + 启停推流)
│ └── src/main/java/com/pengxh/app/rtsp/publisher
│ ├── PermissionActivity.kt # CAMERA 权限申请(EasyPermissions),授权后跳转 MainActivity
│ ├── MainActivity.kt # 编码器 + RTSP 服务串联示例(发布/停止按钮、水印开关)
│ ├── StreamConfig.kt # RTSP 地址拼装(Gson + SaveKeyValues 持久化),当前示例未接入
│ └── PublisherApplication.kt # Application,初始化 SaveKeyValues
├── encoder/ # 纯 Java 采集编码库(Camera2 + GL + MediaCodec)
│ └── src/main/java/com/pengxh/encoder
│ ├── VideoEncoder.java # 编码引擎门面
│ ├── VideoEncoderConfig.java # 参数配置(Builder;宽高可经 updateSize 变更)
│ ├── H264DataCallback.java # H.264 数据回调(onFrameEncoded / onEncoderError)
│ ├── EncoderState.java # IDLE / STARTING / ENCODING / STOPPING
│ ├── SizeSelector.java # 相机输出尺寸选配
│ └── gl/ # EGL 环境与滤镜管线
│ ├── EglCore.java # EGL Display / Context / WindowSurface 封装
│ ├── EglRenderLayer.java # GL 层门面:创建 RenderThread 并对外暴露相机输入 Surface
│ ├── FrameEglRenderer.java # 渲染器:OES 纹理采样 → 滤镜 → 预览/编码 Surface
│ ├── EglRenderFilter.java # 滤镜接口(init / onDraw / release)
│ ├── PassThroughFilter.java # 直通滤镜(filter 传 null 时的默认实现)
│ ├── WatermarkFilter.java # 水印滤镜(单位 / 位置 / 时间)
│ ├── WatermarkController.java # 水印开关与内容更新(任意线程调用,投递到 GL 线程)
│ ├── WatermarkConfig.java # 水印配置(Builder)
│ ├── ShaderProgram.java # GLSL 程序编译链接工具
│ └── FloatArray.java # float[] → FloatBuffer 工具
├── publisher/ # RTSP 发布库(Kotlin 门面 + C++/live555)
│ ├── src/main/java/com/pengxh/media
│ │ ├── RtspPublisher.kt # Kotlin API(object 单例)
│ │ ├── RtspStatus.kt # 状态码常量
│ │ └── RtspStatusCallback.kt # 状态回调接口
│ ├── src/main/jniLibs// # 预编译 live555 静态库 ×4 ABI
│ └── src/main/cpp
│ ├── CMakeLists.txt # C++14,链接 live555 四库,定义 NO_OPENSSL=1
│ ├── RtspPublisher.cpp # JNI 入口(4 个 native 方法)
│ ├── include/ # live555 头文件(181 个 .hh + NetCommon.h)
│ └── src
│ ├── global_definition.hpp # RtspStatus 枚举 / H264Frame 结构
│ ├── java_callback.{hpp,cpp} # JNI 回调(AttachCurrentThread + 全局引用)
│ ├── util/h264_splitter.{hpp,cpp} # 格式探测 / AVCC→AnnexB / SPS·PPS 提取
│ ├── util/logger.{hpp,cpp} # logcat 边框日志
│ ├── util/frame_queue.{hpp,cpp} # 有界帧队列 + per-consumer 消费游标
│ ├── rtsp/
│ │ ├── rtsp_manager.{hpp,cpp} # 单例调度中心
│ │ └── live_rtsp_server.{hpp,cpp} # RTSPServer 生命周期 + 事件循环线程
│ └── server/
│ ├── live_framed_source.{hpp,cpp} # 自定义 FramedSource
│ └── live_server_media_session.{hpp,cpp} # SDP / RTPSink / 首客户端起播回调
├── build_live555.sh # WSL 下交叉编译 live555 全平台静态库
└── config.android.* # live555 交叉编译配置(4 个 ABI)
```
> native 目录按职责分层:`rtsp/` 负责服务与调度,`server/` 负责会话与数据源,`util/` 是与 RTSP
> 无关的可复用工具。
>
> `include/` 与 `jniLibs/` 是 `build_live555.sh` 的**产物拷贝**,不属于本项目源码,不要手工编辑。
### 头文件 Guard 宏约定
所有 native 头文件统一使用 **路径映射式** guard,命名格式为:
```
RTSPPUBLISHER_PUBLISHER_SRC_MAIN_CPP_SRC_<目录>_<文件名>_HPP_
```
即 `RtspPublisher` 项目名 + 从 `publisher/src/main/cpp/` 起的目录路径 + 文件名,全大写、分隔符转 `_`。
| 头文件 | Guard 宏 |
|--------------------------------------------|----------------------------------------------------------------------------------|
| `src/global_definition.hpp` | `RTSPPUBLISHER_PUBLISHER_SRC_MAIN_CPP_SRC_GLOBAL_DEFINITION_HPP_` |
| `src/java_callback.hpp` | `RTSPPUBLISHER_PUBLISHER_SRC_MAIN_CPP_SRC_JAVA_CALLBACK_HPP_` |
| `src/util/logger.hpp` | `RTSPPUBLISHER_PUBLISHER_SRC_MAIN_CPP_SRC_UTIL_LOGGER_HPP_` |
| `src/util/h264_splitter.hpp` | `RTSPPUBLISHER_PUBLISHER_SRC_MAIN_CPP_SRC_UTIL_H264_SPLITTER_HPP_` |
| `src/util/frame_queue.hpp` | `RTSPPUBLISHER_PUBLISHER_SRC_MAIN_CPP_SRC_UTIL_FRAME_QUEUE_HPP_` |
| `src/rtsp/rtsp_manager.hpp` | `RTSPPUBLISHER_PUBLISHER_SRC_MAIN_CPP_SRC_RTSP_RTSP_MANAGER_HPP_` |
| `src/rtsp/live_rtsp_server.hpp` | `RTSPPUBLISHER_PUBLISHER_SRC_MAIN_CPP_SRC_RTSP_LIVE_RTSP_SERVER_HPP_` |
| `src/server/live_framed_source.hpp` | `RTSPPUBLISHER_PUBLISHER_SRC_MAIN_CPP_SRC_SERVER_LIVE_FRAMED_SOURCE_HPP_` |
| `src/server/live_server_media_session.hpp` | `RTSPPUBLISHER_PUBLISHER_SRC_MAIN_CPP_SRC_SERVER_LIVE_SERVER_MEDIA_SESSION_HPP_` |
**移动或重命名头文件时必须同步修改 guard 宏**,否则宏名会与实际路径脱节,极端情况下还可能与其他头文件撞名。
---
## 环境要求
| 项 | 版本 |
|------------------------|------------------------------------------|
| Android Gradle Plugin | 8.11.1 |
| Kotlin | 2.3.20 |
| Gradle Wrapper | 8.13 |
| compileSdk / targetSdk | 36 |
| minSdk | 26(Android 8.0) |
| JDK | 11 |
| NDK | 21.4.7075529 |
| CMake | 3.22.1(`CMakeLists.txt` 最低要求 3.16) |
| C++ 标准 | C++14 |
| 支持 ABI | `armeabi-v7a`、`arm64-v8a`、`x86`、`x86_64` |
| live555 | 2026.08.25(预编译静态库,以 `-DNO_OPENSSL=1` 编译) |
示例 App:`applicationId = com.pengxh.app.rtsp.publisher`,`versionCode = 1000`,
`versionName = 1.0.0.0`。
### 依赖
| 模块 | 依赖 |
|-------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `app` | `project(':encoder')`、`project(':publisher')`、`Kotlin-lite-lib:2.0.0`、`androidx.core:core-ktx:1.17.0`、`appcompat:1.7.1`、`material:1.13.0`、`easypermissions:3.0.0`、`gson:2.14.0` |
| `encoder` | `androidx.core:core:1.17.0`、`appcompat:1.7.1`、`material:1.13.0` |
| `publisher` | `androidx.core:core-ktx:1.17.0`(native 侧仅链接 `android`、`log` 与 live555 四库) |
### 权限
`CAMERA`(必需)、`INTERNET`、`ACCESS_NETWORK_STATE`、`ACCESS_WIFI_STATE`、`BACKGROUND_CAMERA`。
`uses-feature`:`android.hardware.camera`(必需)、`android.hardware.camera.autofocus`(可选)。
---
## 快速开始
### 1. 构建与安装
```bash
./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/RtspPublisher_<日期>_1.0.0.0.apk
```
产物命名格式:`RtspPublisher_yyyyMMdd_1.0.0.0.apk`。
### 2. 运行
1. 授权相机权限后进入主界面,可看到本地预览(相机此时已在采集,但**未编码**)。
2. 点击 **发布RTSP流**:App 先 `setEncodingEnable(true)` 开启编码,再调用
`RtspPublisher.initRtsp("live", 720, 1280, 30, 3_000_000)`。服务端在收到首个同时满足
「已解析出 SPS/PPS」且「包含 slice」的帧后才真正启动。
3. 回调返回 `INIT_SUCCESS`,`message` 即为可播放地址,例如:
```
rtsp://192.168.1.23:8554/live
```
4. 有客户端真正拉流后回调 `STREAM_STARTED`。点击 **停止RTSP流** 则 `RtspPublisher.release()`
并 `setEncodingEnable(false)`。
5. 主界面的水印复选框通过 `WatermarkController.setEnabled()` 实时开关水印。
### 3. 拉流验证
```bash
# 低延迟(推荐)
ffplay -fflags nobuffer -flags low_delay -framedrop -analyzeduration 0 -probesize 32 -rtsp_transport tcp rtsp://192.168.137.195:8554/live
# 或 VLC:媒体 → 打开网络串流
```
> 手机与播放器需处于同一局域网;手机热点/企业网络的 AP 隔离会导致拉流失败。
---
## API 使用
### publisher 模块(`com.pengxh.media`)
```kotlin
// 1. 注册状态回调(必须在 initRtsp 之前)
RtspPublisher.registerStatusCallback(object : RtspStatusCallback {
override fun onStatusChanged(code: Int, message: String) {
when (code) {
RtspStatus.INIT_SUCCESS -> { /* message = rtsp://ip:8554/live,此时服务端已在推流 */
}
RtspStatus.INIT_FAILED -> { /* 服务启动失败(端口占用 / SPS·PPS 缺失等) */
}
RtspStatus.STREAM_STARTED -> { /* 首个客户端真正开始拉流(PLAY 后触发) */
}
RtspStatus.STREAM_FAILED -> { /* 预留:当前 native 侧未触发 */
}
RtspStatus.RELEASED -> { /* 已释放 */
}
}
}
})
// 2. 初始化(streamName 为空时回落为 "live")
RtspPublisher.initRtsp(
streamName = "live",
width = 720,
height = 1280,
fps = 30,
bitrate = 3_000_000
)
// 3. 逐帧送入编码数据(buffer 必须是 direct ByteBuffer)
RtspPublisher.pushVideoFrameBuffer(buffer, size, ptsUs, isKeyFrame)
// 4. 释放
RtspPublisher.release()
```
| 方法 | 说明 |
|---------------------------------------------------------|---------------------------------------------------------------------------|
| `registerStatusCallback(cb)` | 注册状态回调,内部持 JNI 全局引用;native 侧对 null 入参做了防护 |
| `initRtsp(streamName, width, height, fps, bitrate)` | 记录参数并复位内部状态(重复调用会先停服再清队列);服务端延迟到拿到 SPS/PPS 后才真正启动 |
| `pushVideoFrameBuffer(buffer, size, ptsUs, isKeyFrame)` | 送一帧 H.264(AVCC / AnnexB 均可);`buffer` 必须是 direct ByteBuffer,`size` 是有效数据长度 |
| `release()` | 停服、清队列、复位状态,并回调 `RELEASED` |
状态码(`RtspStatus`):`INIT_SUCCESS=0`、`INIT_FAILED=1`、`STREAM_STARTED=2`、`STREAM_FAILED=3`、
`RELEASED=4`。
- 未调用 `initRtsp()` 时 `handle_video_frame()` 直接返回,**推流前必须先初始化**。
- `INIT_SUCCESS` 的 `message` 由 live555 `rtspURL()` 生成,形如 `rtsp://<本机IP>:8554/live`。
- `STREAM_STARTED` 由 `LiveServerMediaSession` 覆写 `startStream()` 触发:服务端监听成功只是
`INIT_SUCCESS`,**要等第一个客户端真正发起拉流** 才会回调 `STREAM_STARTED`(每个会话只通知一次)。
若需要统计在线客户端,可在此基础上扩展计数。
- `STREAM_FAILED=3` 与 native 侧的 `_error_count` / `_too_many_errors_notified` 是**预留**字段,
当前代码路径没有任何一处发出该状态,上层可以只做兜底处理。
- RTSP 端口固定为 `8554`(`RtspManager::_rtsp_port` 无对外 setter),**暂不可配置**。
### encoder 模块(`com.pengxh.encoder`)
```java
VideoEncoderConfig config = new VideoEncoderConfig.Builder()
.setCameraFacing(CameraCharacteristics.LENS_FACING_BACK)
.setWidth(720)
.setHeight(1280)
.setFrameRate(30)
.setBitrate(3_000_000)
.setIFrameInterval(1) // 单位秒,-1 表示仅首帧
.build();
VideoEncoder encoder = new VideoEncoder(activity, config, textureView, filter, callback);
```
| 方法 | 说明 |
|------------------------------|----------------------------------------------------------------------|
| `VideoEncoder(...)` | 构造:`filter` 传 `null` 使用默认直通滤镜;`config` 传 `null` 使用 `createDefault()` |
| `start()` | 初始化引擎,异步开相机并出预览 |
| `setEncodingEnable(boolean)` | 启停编码;停止后自动重建纯预览 CaptureSession |
| `getEncoderState()` | 返回 `EncoderState`:`IDLE / STARTING / ENCODING / STOPPING` |
| `getWatermarkController()` | 获取水印控制器(返回 `null` 表示未使用 `WatermarkFilter`) |
| `release()` | 幂等释放:相机、MediaCodec、GL 层、线程 |
数据回调(`H264DataCallback`):
```java
public interface H264DataCallback {
void onFrameEncoded(@NonNull ByteBuffer buffer, int size, long ptsUs, boolean isKeyFrame);
void onEncoderError(@NonNull Exception e);
}
```
`VideoEncoderConfig` 的 Builder 对非法参数直接抛 `IllegalArgumentException`(宽高必须为正偶数、
帧率/码率 > 0、关键帧间隔 >= -1)。除 `updateSize()` 外的字段都是 `final`;`updateSize()` 目前
只改配置对象本身,不会重建已运行的编码器。
水印控制(`WatermarkController`,可在任意线程调用,内部投递到 GL 线程):
自定义滤镜实现 `EglRenderFilter` 即可接入 GL 管线(处理必须发生在 GL 线程):
```java
public interface EglRenderFilter {
void init();
int onDraw(int inputTexture, float[] transformMatrix, int width, int height, long ptsUs);
void release();
}
```
`onDraw()` 返回值是处理后的纹理 id,GL 层会把它渲染到预览与编码 Surface。两种内置实现:
- `PassThroughFilter`:内部 FBO,输出与输入同尺寸的旋转后纹理;`filter` 传 `null` 时默认使用它。
- `WatermarkFilter`:先用 `PassThroughFilter` 得到主画面 FBO 纹理,再把文字水印
(单位 / 位置 / 时间,Canvas 绘成 Bitmap 后上传为 2D 纹理)叠加到同一个 FBO 上,因此返回值仍是主画面纹理
id。
水印内容仅在文本变化时重绘 Bitmap,时间戳按秒刷新。
---
## 关键实现说明
### 1. 格式归一化
`h264_splitter` 自动探测输入格式:
- **AnnexB**(`00 00 00 01` / `00 00 01` 起始码)→ 直接使用;
- **AVCC**(4 字节大端长度前缀)→ `avcc_to_annex_b()` 转为起始码格式;
- 其他 → 丢弃并告警。
`h264_splitter` 对外提供:`detect_format()`、`avcc_to_annex_b()`、`extract_sps()`、
`extract_pps()`、`extract_sps_pps()`、`contains_slice()`、`contains_idr()`。
其中 `extract_*()` 返回的净荷**不含起始码**。
帧内若无 slice NALU 则直接返回,避免把纯参数集当画面帧写入。
### 2. SPS/PPS 与 SDP
- `update_codec_config()` 只在解析出的 SPS/PPS 与已缓存值**不同**时置 `config_changed`,
避免每帧都打日志和触发重建。
- 首帧解析出的 SPS/PPS 通过 `H264VideoRTPSink::createNew(..., sps, pps)` 生成
SDP 中的 `sprop-parameter-sets`,播放器无需等待带内参数集即可建流。
- SPS/PPS 发生变化时置 `_codec_config_changed`,并 **等到下一个 IDR 帧** 才停服、清队列、重启会话,
保证新客户端拿到的是与新码流匹配的描述,旧客户端也不会解码半截配置。
- `start_rtsp_server()` 的硬门槛是 `_sps` 与 `_pps` 均非空,否则直接返回 `false`。
### 3. 帧队列
`FrameQueue`(`util/frame_queue.hpp`)是有界环形缓冲(`kMaxQueueFrames = 30`,约 1 秒 @30fps),
本身是进程内单例,`push()` / `pop()` 由互斥锁保护:
- 生产者 `push()` 超限时 `pop_front()` 丢弃最老帧,防止内存增长;
- 每个消费者持有 `Reader{next_index}` 游标,`pop()` 按全局序号 `_total_pushed` 取帧,多客户端互不干扰;
- 消费者落后到缓存范围之外时,先跳到最新一帧,再回扫定位到缓存中的**第一个**关键帧;
- `on_client_attach()` 从后往前扫描,让新接入的客户端从缓存中**最近**的关键帧起播;
缓存中没有关键帧时回退到最新一帧,降低首屏等待。
- 入队时 `is_key_frame` 取 `isKeyFrame || contains_idr()`,双重保险。
### 4. 时间戳
```cpp
if (_base_pts_us < 0) _base_pts_us = pts_us;
int64_t normalized_pts_us = pts_us - _base_pts_us;
if (normalized_pts_us < 0) normalized_pts_us = 0;
int64_t pts_90kHz = normalized_pts_us * 90000 / 1000000; // 折算到 RTP 90kHz 时间基
if (_last_pts_90k >= 0 && pts_90kHz <= _last_pts_90k) {
pts_90kHz = _last_pts_90k + 1; // 强制单调递增,避免花屏
}
_last_pts_90k = pts_90kHz;
normalized_pts_us = _last_pts_90k * 1000000 / 90000; // 反算回微秒再入队
```
`LiveFramedSource::deliver_from_queue()` 把 `pts_us` 拆成 `timeval` 写入 `fPresentationTime`,
并设置 `fDurationInMicroseconds = 1000000 / fps`。
### 5. JNI 边界
`GetDirectBufferCapacity()` 返回的是 **底层 buffer 总容量**(通常远大于一帧),
因此 Java 侧必须传入 `MediaCodec.BufferInfo.size` 作为 `size`,
native 只按 `size` 拷贝有效区间,并做 `size <= 0`、`size > capacity`、非 direct buffer 三类防护。
4 个 native 方法:`registerStatusCallback`、`initRtsp`、`pushVideoFrameBuffer`、`release`。
### 6. 停止流程
`LiveRtspServer::stop()` 先经 `triggerEvent(wakeupTriggerId)` 唤醒阻塞在 `doEventLoop` 的线程,
由 `handle_wakeup_event` 置位 `_loop_watch` 使循环退出,再 join 线程并逐层释放
(trigger → ServerMediaSession → RTSPServer → Environment / TaskScheduler),无残留线程。
`stop()` 在 `_scheduler_ptr == nullptr` 时直接返回,且 join 前会判断调用者是不是事件循环线程自身,
因此**重复调用是安全的**。
### 7. 回调时序
`RtspManager` 内部所有状态计算都在 `_rtsp_mutex` 保护下完成,只在锁外才调
`notify_status()`。这样即使 `onStatusChanged()` 里反过来调用了 `release()` /
`pushVideoFrameBuffer()`,也不会自死锁。
---
## 重新编译 live555
预编译静态库已随仓库提供(4 个 ABI),通常无需重新编译。如需升级 live555,在 **WSL** 中执行:
```bash
# 1. 把 config.android.{armeabi-v7a,arm64-v8a,x86,x86_64} 放到 live555 源码根目录
# 2. 修改 build_live555.sh 顶部的 LIVE_SRC 路径
# 3. 执行
./build_live555.sh
```
脚本要点:
- 逐 ABI 复制源码到独立构建目录(`build_/`),避免污染源码树;
- 使用 `./genMakefiles android.` 生成 Makefile,编译
`UsageEnvironment` / `groupsock` / `liveMedia` / `BasicUsageEnvironment` 四个库;
- 自动打 **C++14 兼容补丁**:把仅 C++20 才有的 `atomic_flag::test()` 替换为 `test_and_set()` 实现;
- 产物收集到 `$OUTPUT_DIR//lib/*.a` 与 `include/`(`*.hh` 与 `*.h` 都会拷)。
配置文件命名:脚本读取的是 `config.android.`,与仓库根目录的 4 个 `config.android.*` 一一对应。
拷贝产物到工程:
```
//lib/*.a → publisher/src/main/jniLibs//
//include/* → publisher/src/main/cpp/include/
```
> 静态库以 `-DNO_OPENSSL=1` 编译,`CMakeLists.txt` 中同步定义了同名宏,**两边必须保持一致**,
> 否则会出现结构体布局不一致导致的运行期问题。
>
> 升级 live555 后若新增/删除了头文件,记得同步检查 `include/` 目录,避免出现悬空 `#include`。
---
## 默认参数
| 参数 | 默认值 | 位置 |
|----------|-----------------------|---------------------------------------------------------|
| RTSP 端口 | `8554`(固定,不可配置) | `rtsp/rtsp_manager.hpp` `kDefaultRtspPort` |
| 挂载点 | `live` | `RtspManager::initialize()`(`streamName` 为空时回落) |
| 摄像头 | 后置(LENS_FACING_BACK) | `VideoEncoderConfig.Builder` |
| 分辨率 | `720 × 1280` | `VideoEncoderConfig.Builder` / `RtspManager` 兜底值 |
| 帧率 | `30` | 同上 |
| 码率 | `3 Mbps`(CBR) | 同上 |
| 关键帧间隔 | `1 s` | `VideoEncoderConfig.Builder` |
| B 帧 | `0`(API 29+) | `VideoEncoder.createVideoFormat()` |
| 色彩 | BT709 / SDR / LIMITED | `VideoEncoder.createVideoFormat()` |
| 编码超时 | `10 ms` | `VideoEncoder.TIMEOUT_US`(`dequeueOutputBuffer`) |
| 帧队列容量 | `30` 帧 | `util/frame_queue.hpp` `kMaxQueueFrames` |
| 取帧轮询间隔 | `5 ms` | `server/live_framed_source.hpp` `kRetryIntervalUs` |
| 单帧上限 | `2 MB` | `LiveFramedSource::maxFrameSize()` |
| SDP 估算码率 | `5000 kbps` | `LiveServerMediaSession::createNewStreamSource()` |
| 会话描述 | `H.264 rtsp stream` | `LiveRtspServer::start()`(`ServerMediaSession` info 字段) |
---
## 注意事项
- **必须在子线程消费编码数据**:`H264DataCallback` 与 MediaCodec 输出缓冲区共享内存,回调返回后数据可能被覆盖;本项目的
`pushVideoFrameBuffer` 内部已做拷贝到 `std::vector`。
- **`size` 一定用 `info.size`**:不要用 `buffer.remaining()` 之外的容量值,更不要把 `capacity` 当长度。
- **先注册回调再 `initRtsp`**,否则拿不到 `INIT_SUCCESS` 中的可播放 URL。
- **先 `initRtsp` 再送帧**:未初始化时 `handle_video_frame()` 直接 return,帧会被静默丢弃。
- **`release()` 幂等**:Activity `onDestroy` 中调用即可,重复调用安全,但每次都会回调一次 `RELEASED`。
- **端口占用**:`8554` 被占用时 `RTSPServer::createNew()` 返回 `nullptr`,会回调 `INIT_FAILED`;
此时 `_init_failure_notified` 置位,只有下次重建成功后才会清除,**不会每帧重复回调**。
- **超大 IDR**:单帧超过 `fMaxSize`(2 MB)会被丢弃,播放器需等待下一个 IDR;高码率/高分辨率场景可适当调大
`maxFrameSize()`。
- **网络**:RTSP 默认走 UDP,弱网建议播放器显式指定 `rtsp_transport tcp`。
- **`STREAM_FAILED` 当前不会触发**:native 侧预留了 `_error_count` / `_too_many_errors_notified`,
但没有任何代码路径发出 `StreamFailed`,不要依赖它做故障恢复。
- **`app/StreamConfig.kt` 未被示例接入**:它是一套独立的 RTSP 地址拼装工具(Gson + `SaveKeyValues`),
与本项目的「设备即服务端」流程无关,可忽略或按需复用。
- **`consumer-rules.pro` / `proguard-rules.pro`**:当前两个 library 模块的 `minifyEnabled` 均为
`false`,发布前若开启混淆需自行补齐 native 方法与回调接口的 keep 规则。
---
## License
见 [LICENSE](LICENSE)。