# RtspPublisher
**Repository Path**: AndroidCoderPeng/RtspPublisher
## Basic Information
- **Project Name**: RtspPublisher
- **Description**: Android 端 内置 RTSP 服务器 的低延迟实时推流方案:手机/平板用 Camera2 采集画面,经 OpenGL 处理(水印/滤镜)后由 MediaCodec 硬编码为 H.264 或 H.265(HEVC),再通过 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-14
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# RtspPublisher
Android 端 **内置 RTSP 服务器** 的低延迟实时推流方案:手机/平板用 Camera2 采集画面,经 OpenGL
处理(水印/滤镜)后由 MediaCodec 硬编码为 H.264 **或** H.265(HEVC),再通过 JNI 交给内嵌的 live555
RTSP 服务对外发布。
与「推流到外部服务器」不同,本项目是 **设备即服务端**:Android 设备自身监听 RTSP 端口(固定 `8554`
),同一局域网内的 VLC / ffplay / 其他播放器可直接拉流,无需任何中转服务器。
```
采集:Camera2 → SurfaceTexture(OES)
处理:OpenGL ES 滤镜管线(水印 / 时间 / 旋转)
编码:MediaCodec 硬编码 H.264 / H.265(COLOR_FormatSurface,零拷贝送帧)
传输:JNI → live555 RTSPServer → RTP/RTCP → 播放器
```
---
## 特性
- **设备即服务端**:内置 live555 `RTSPServer`,无需搭建流媒体服务器,局域网直连即可播放。
- **双编码格式**:同时支持 **H.264 (AVC)** 与 **H.265 (HEVC)**。编码类型由
`setVideoCodec("H264" | "H265")`
指定(默认 H.264),native 据此选择 `H264VideoRTPSink` / `H265VideoRTPSink` 与对应的 DiscreteFramer。
- **硬编码**:MediaCodec `COLOR_FormatSurface` 输入,全程 Surface 通路,避免 CPU 拷贝 YUV。
- **OpenGL 处理管线**:相机帧先经 GL 渲染再送入编码器,可挂载任意 `EglRenderFilter`
(水印、时间、贴纸、美颜、旋转等);内置 `PassThroughFilter` 直通滤镜与 `WatermarkFilter` 水印滤镜。
- **格式自适应**:JNI 层自动识别 AVCC / AnnexB,统一归一化为 AnnexB 后再交给 live555;H.264 / H.265 的
NALU
类型解析规则各自适配(H.264 取低 5 bit,H.265 取首字节右移 1 位后的低 6 bit)。
- **参数集注入**:通过 `setVideoParameterSets(csd0, csd1)` 预注入 SPS/PPS(H.265 为 VPS/SPS/PPS),
native 据此生成 SDP `sprop-parameter-sets`;首帧关键帧到达前不对外发流,避免花屏。
- **即时启动**:`initRtsp(streamName, fps)` 立即拉起 RTSP 服务(端口 + 会话),成功后回调
`RTSP_SERVER_STARTED`(message 为可播放 URL);参数集缺失 / 端口占用时启动失败回调
`RTSP_SERVER_STOPPED`。
- **多客户端并发**:有界环形帧队列 + per-consumer 消费游标,每个 `FramedSource` 独立记录读取进度,互不影响。
- **新客户端秒开**:`LiveFramedSource` 构造时即调用 `on_client_attach()`
,把游标定位到缓存中最近的关键帧起播;缓存无关键帧时回退到最新一帧。
- **时间戳直通**:微秒 PTS 由 Java 端直接传入 native,不做归一化折算,按 `fps` 估算
`fDurationInMicroseconds`
后直接写入 `fPresentationTime`。
- **线程安全**:编码线程 / JNI 线程 / live555 事件循环线程通过互斥锁与原子标志隔离;`stop()` 经
`triggerEvent`
(唤醒退出 + 新帧唤醒两个独立 trigger)唤醒阻塞中的事件循环,退出干净。所有 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/H.265]
MC --> CB[FrameDataCallback
零拷贝 ByteBuffer]
MC -. format changed .-> CB2[onVideoOutputFormatChanged]
CB2 --> JNI2[JNI setVideoParameterSets]
CB --> JNI[JNI pushVideoFrameBuffer]
end
JNI2 --> CP[cache_parameter_sets
提取 VPS/SPS/PPS]
CP -->|成功| INIT_OK[回调 INIT_SUCCESS]
CP -->|失败| INIT_FAIL[回调 INIT_FAILED]
INIT_OK -.-> INIT[JNI initRtsp streamName,fps]
INIT --> RM[RtspManager::initialize]
JNI --> DET[detect_format]
DET -->|AVCC| CV[avcc_to_annex_b 归一化为 AnnexB]
DET -->|AnnexB| DIRECT[直接使用]
DET -->|Unknown| DROP[丢弃并告警]
CV --> WVF[RtspManager::write_video_frame]
DIRECT --> WVF
subgraph native 调度
WVF --> SPLIT{frame_splitter
split_frame}
SPLIT --> CLASS{分类 NALU}
CLASS -->|VPS/SPS/PPS| CACHE[更新参数集缓存]
CLASS -->|IDR 帧| IDR[关键帧入队]
CLASS -->|Slice| SLICE[非关键帧入队]
CACHE --> Q[(FrameQueue
有界环形缓冲 60 帧
per-consumer 游标)]
IDR --> Q
SLICE --> Q
RM --> SRV
end
subgraph live555 事件循环线程
SRV[RTSPServer :8554] --> SMS[ServerMediaSession /live]
SMS --> SUB[LiveServerMediaSession
OnDemandServerMediaSubsession]
SUB -->|首客户端 startStream| STARTED[回调 StreamStarted]
SUB --> SRC[LiveFramedSource
triggerEvent 唤醒 + 5ms 轮询兜底]
SRC --> FR{H264/H265
DiscreteFramer}
FR --> SINK[H264/H265VideoRTPSink
SDP 带 sprop-parameter-sets]
SINK --> CLIENT[VLC / ffplay 拉流]
end
SRV -->|启动成功| OK[回调 RTSP_SERVER_STARTED
message = rtsp://ip:8554/live]
SRV -->|参数缺失/端口占用| FAIL[回调 RTSP_SERVER_STOPPED]
Q -->|triggerEvent 唤醒| SRC
STOP[release / watchVariable
+ triggerEvent 唤醒] --> SRV
```
### 线程模型
| 线程 | 职责 |
|---------------------------------------------------|--------------------------------------------------------------------------------------------|
| `CodecWorker`(HandlerThread,`VideoEncoder` 创建) | 相机开关、CaptureSession 构建、MediaCodec 配置/启停 |
| `CodecDrainThread` | `dequeueOutputBuffer` 循环取编码输出,经 `FrameDataCallback` 上抛(随 `startEncode()` 新建) |
| `RenderThread`(HandlerThread,`EglRenderLayer` 创建) | OES 纹理采样、滤镜/水印绘制、渲染到预览与编码 Surface |
| 调用 `pushVideoFrameBuffer` 的线程(通常是 drain 线程) | 格式归一化、参数集/IDR 维护、入帧队列 |
| live555 事件循环线程(`LiveRtspServer` 创建) | `doEventLoop`;`LiveFramedSource` 经 `FrameQueue` 的 `triggerEvent` 跨线程唤醒取帧并 RTP 打包(5ms 轮询兜底) |
`pushVideoFrameBuffer` 与 `RtspManager` 之间的并发由 `_rtsp_mutex` 串行化;
live555 事件循环线程只通过 `FrameQueue` 与其交互,不直接触碰 `RtspManager` 状态。
`LiveFramedSource` 取帧由 `FrameQueue` 在 `push()` 锁外触发 `triggerEvent(_frame_trigger_id)` 唤醒,
5ms 轮询仅为兜底。
---
## 目录结构
```
RtspPublisher
├── app/ # 示例 App(权限 + 预览 + 启停推流 + 编码切换)
│ └── src/main/java/com/pengxh/app/rtsp/publisher
│ ├── PermissionActivity.kt # CAMERA 权限申请(EasyPermissions),授权后跳转 MainActivity
│ ├── MainActivity.kt # 编码器 + RTSP 服务串联示例(发布/停止按钮、水印开关、H.264/H.265 切换)
│ └── PublisherApplication.kt # Application,初始化 SaveKeyValues
├── encoder/ # 纯 Java 采集编码库(Camera2 + GL + MediaCodec)
│ └── src/main/java/com/pengxh/encoder
│ ├── VideoEncoder.java # 编码引擎门面(构造含 VideoCodecType)
│ ├── VideoEncoderConfig.java # 参数配置(Builder;宽高可经 updateSize 变更)
│ ├── VideoCodecType.java # 硬编码器类型枚举:H264 / H265
│ ├── FrameDataCallback.java # 编码数据回调(onFrameEncoded / onVideoOutputFormatChanged / 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 线程)
│ ├── ShaderProgram.java # GLSL 程序编译链接工具
│ └── FloatArray.java # float[] → FloatBuffer 工具
├── publisher/ # RTSP 发布库(Kotlin 门面 + C++/live555)
│ ├── src/main/java/com/pengxh/media
│ │ ├── RtspPublisher.kt # Kotlin API(object 单例,6 个 native 方法)
│ │ ├── RtspStatus.kt # 状态码常量(7 个)
│ │ └── RtspStatusCallback.kt # 状态回调接口
│ ├── src/main/jniLibs// # 预编译 live555 静态库 ×4 ABI
│ └── src/main/cpp
│ ├── CMakeLists.txt # C++14,链接 live555 四库,定义 NO_OPENSSL=1
│ ├── RtspPublisher.cpp # JNI 入口(6 个 native 方法)
│ ├── include/ # live555 头文件(181 个 .hh + NetCommon.h)
│ └── src
│ ├── global_definition.hpp # RtspStatus 枚举 / VideoCodec / NALU / VideoFrame 结构
│ ├── java_callback.{hpp,cpp} # JNI 回调(AttachCurrentThread + 全局引用)
│ ├── util/frame_splitter.{hpp,cpp} # 格式探测 / AVCC→AnnexB / NALU 拆分与类型解析
│ ├── util/logger.{hpp,cpp} # logcat 边框日志
│ ├── util/frame_queue.{hpp,cpp} # 有界帧队列 + per-consumer 消费游标
│ ├── rtsp/
│ │ ├── rtsp_manager.{hpp,cpp} # 单例调度中心(参数集缓存 / IDR 等待 / 分发)
│ │ └── live_rtsp_server.{hpp,cpp} # RTSPServer 生命周期 + 事件循环线程
│ └── server/
│ ├── live_framed_source.{hpp,cpp} # 自定义 FramedSource(triggerEvent 唤醒 + 5ms 轮询)
│ └── live_server_media_session.{hpp,cpp} # SDP / RTPSink(H.264/H.265)/ 首客户端起播回调
├── 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/frame_splitter.hpp` | `RTSPPUBLISHER_PUBLISHER_SRC_MAIN_CPP_SRC_UTIL_FRAME_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. 主界面默认 H.264;点选 H.265 单选框会通过 `setVideoCodec("H265")` 切换封装(需先停止推流)。
3. 点击 **发布RTSP流**:App 先 `setEncodingEnable(true)` 开启编码;编码器就绪后回调
`onVideoOutputFormatChanged`,App 取出 `csd-0/csd-1` 调
`RtspPublisher.setVideoParameterSets(...)`,
成功后回调 `INIT_SUCCESS`,App 随即调用 `RtspPublisher.initRtsp("live", 30)` 拉起服务。
4. 服务端拉起成功后回调 `RTSP_SERVER_STARTED`,`message` 即为可播放地址,例如:
```
rtsp://192.168.1.23:8554/live
```
5. 有客户端真正拉流后回调 `STREAM_STARTED`。再次点击按钮则 `RtspPublisher.release()`
并 `setEncodingEnable(false)`。
6. 主界面的水印复选框通过 `WatermarkController.setEnabled()` 实时开关水印。
### 3. 拉流验证
- H.264 拉流命令
```bash
ffplay -fflags nobuffer -flags low_delay -framedrop -analyzeduration 0 -probesize 32 -rtsp_transport tcp rtsp://192.168.137.195:8554/live
```
- H.265 拉流命令
```bash
ffplay -rtsp_transport tcp rtsp://192.168.137.36:8554/live
```
- 或 VLC:媒体 → 打开网络串流
> 手机与播放器需处于同一局域网;手机热点/企业网络的 AP 隔离会导致拉流失败。
## API 使用
### publisher 模块(`com.pengxh.media`)
```kotlin
// 0. 选择编码格式(必须在 setVideoParameterSets / initRtsp 之前;默认 H264)
RtspPublisher.setVideoCodec("H264") // "H264" 或 "H265"
// 1. 注册状态回调(必须在 initRtsp 之前)
RtspPublisher.registerStatusCallback(object : RtspStatusCallback {
override fun onStatusChanged(code: Int, message: String) {
when (code) {
RtspStatus.INIT_SUCCESS -> { /* 参数集注入成功(csd-0/csd-1 已缓存) */
}
RtspStatus.INIT_FAILED -> { /* 参数集注入失败 */
}
RtspStatus.RTSP_SERVER_STARTED -> { /* message = rtsp://ip:8554/live,服务端已就绪 */
}
RtspStatus.RTSP_SERVER_STOPPED -> { /* 启动失败(端口占用 / 参数集缺失等)或主动 release */
}
RtspStatus.STREAM_STARTED -> { /* 首个客户端真正开始拉流(PLAY 后触发) */
}
RtspStatus.STREAM_FAILED -> { /* 预留:当前 native 侧未触发 */
}
RtspStatus.RELEASED -> { /* 已释放 */
}
}
}
})
// 2. 预注入视频参数集(编码器输出格式变化回调时调用,见 FrameDataCallback.onVideoOutputFormatChanged)
// H.264: csd0=[SPS][PPS],csd1 一般为 null
// H.265: csd0=[VPS][SPS],csd1=[PPS]
RtspPublisher.setVideoParameterSets(csd0, csd1)
// 3. 启动 RTSP 服务(streamName 为空时回落为 "live")
RtspPublisher.initRtsp(
streamName = "live",
fps = 30
)
// 4. 逐帧送入编码数据(buffer 必须是 direct ByteBuffer;AVCC / AnnexB 均可;无需显式传 isKeyFrame)
RtspPublisher.pushVideoFrameBuffer(buffer, size, ptsUs)
// 5. 释放
RtspPublisher.release()
```
| 方法 | 说明 |
|---------------------------------------------|----------------------------------------------------------------------------------------------------------------|
| `registerStatusCallback(cb)` | 注册状态回调,内部持 JNI 全局引用;native 侧对 null 入参做了防护 |
| `setVideoCodec(codec)` | 设置编码格式:`"H264"`(默认)或 `"H265"`;必须在 `setVideoParameterSets` / `initRtsp` 之前调用 |
| `setVideoParameterSets(csd0, csd1)` | 预注入参数集(H.264: SPS/PPS 走 csd0,PPS 也可走 csd1;H.265: csd0=VPS+SPS,csd1=PPS)。成功回调 `INIT_SUCCESS`,失败回调 `INIT_FAILED` |
| `initRtsp(streamName, fps)` | 立即拉起 RTSP 服务;成功回调 `RTSP_SERVER_STARTED`(message 为可播放 URL),参数集缺失 / 端口占用则回调 `RTSP_SERVER_STOPPED`。重复调用会先停服再重启 |
| `pushVideoFrameBuffer(buffer, size, ptsUs)` | 送一帧(AVCC / AnnexB 均可);`buffer` 必须是 direct ByteBuffer,`size` 是有效数据长度;native 自动识别格式并归一化为 AnnexB |
| `release()` | 停服、清队列、复位状态(含参数集缓存),并回调 `RELEASED` |
状态码(`RtspStatus`):`INIT_SUCCESS=0`、`INIT_FAILED=1`、`RTSP_SERVER_STARTED=2`、
`RTSP_SERVER_STOPPED=3`、`STREAM_STARTED=4`、`STREAM_FAILED=5`、`RELEASED=6`。
- 未成功调用 `setVideoParameterSets`(即参数集缓存为空)时 `initRtsp` 启动失败,回调
`RTSP_SERVER_STOPPED`;`write_video_frame` 在参数集为空时也会直接丢弃帧。
- `RTSP_SERVER_STARTED` 的 `message` 由 live555 `rtspURL()` 生成,形如 `rtsp://<本机IP>:8554/live`。
- `STREAM_STARTED` 由 `LiveServerMediaSession` 覆写 `startStream()` 触发:服务端启动成功只是
`RTSP_SERVER_STARTED`,**要等第一个客户端真正发起拉流** 才会回调 `STREAM_STARTED`(每个会话只通知一次)。
若需要统计在线客户端,可在此基础上扩展计数。
- `RTSP_SERVER_STOPPED` 既可能因启动失败触发,也可能在重复 `initRtsp` 先停服、或 `release()` 时触发。
- `STREAM_FAILED=5` 与 native 侧是**预留**字段,当前代码路径没有发出该状态,上层可只做兜底处理。
- RTSP 端口固定为 `8554`(`RtspManager::_rtsp_port` 无对外 setter),**暂不可配置**。
- 关键帧由 native 在 `write_video_frame` 内部自动识别(H.264 的 slice type=5,H.265 的 type 16–21),
**无需 Java 侧传 isKeyFrame**。
### 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, codecType, callback);
```
| 方法 | 说明 |
|--------------------------------|---------------------------------------------------------------------------------------------------------------------|
| `VideoEncoder(...)` | 构造:`filter` 传 `null` 使用默认直通滤镜;`config` 传 `null` 使用 `createDefault()`;`codecType` 为 `VideoCodecType` 枚举(必填,不可为 null) |
| `start()` | 初始化引擎,异步开相机并出预览 |
| `setEncodingEnable(boolean)` | 启停编码;停止后自动重建纯预览 CaptureSession |
| `setCodecType(VideoCodecType)` | 仅在未编码 / 已停止状态下切换编码格式;编码中调用抛 `IllegalStateException` |
| `getEncoderState()` | 返回 `EncoderState`:`IDLE / STARTING / ENCODING / STOPPING` |
| `getWatermarkController()` | 获取水印控制器(返回 `null` 表示未使用 `WatermarkFilter`) |
| `release()` | 幂等释放:相机、MediaCodec、GL 层、线程 |
数据回调(`FrameDataCallback`):
```java
public interface FrameDataCallback {
void onFrameEncoded(@NonNull ByteBuffer buffer, int size, long ptsUs, boolean isKeyFrame);
void onVideoOutputFormatChanged(@NonNull MediaFormat format); // 首帧前必回调,携带 csd-0 / csd-1
void onEncoderError(@NonNull Exception e);
}
```
`onVideoOutputFormatChanged` 必须实现——把 `format` 的 `csd-0` / `csd-1`(ByteBuffer)取出后调用
`RtspPublisher.setVideoParameterSets(csd0, csd1)`(H.265 的 VPS+SPS 在 csd-0、PPS 在 csd-1)。
`onFrameEncoded` 中的 `isKeyFrame` native 已不再依赖,但仍由 MediaCodec 提供。
`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. 格式归一化
`frame_splitter` 自动探测输入格式:
- **AnnexB**(`00 00 00 01` / `00 00 01` 起始码)→ 直接使用;
- **AVCC**(4 字节大端长度前缀)→ `avcc_to_annex_b()` 转为起始码格式;
- 其他 → 丢弃并告警。
`frame_splitter` 对外提供:`detect_format()`、`avcc_to_annex_b()`、`split_frame()`。
`split_frame()` 按起始码切分 NALU,并按 `VideoCodec` 解析 NALU 类型:
- H.264:`type = data[0] & 0x1F`;
- H.265:`type = (data[0] >> 1) & 0x3F`(2 字节 NAL 头,6 bit 类型)。
`RtspManager::write_video_frame` 收到完整 AnnexB 访问单元后调用 `split_frame` 拆分为 NALU 并分类:
SPS/PPS(H.265 含 VPS)、IDR 帧、普通 slice;纯参数集不入队,仅更新缓存。
### 2. 参数集与 SDP
- `cache_parameter_sets()` 把 `csd0/csd1` 经 `split_frame` 拆出 VPS/SPS/PPS 缓存;
H.265 要求三者齐全,H.264 要求 SPS/PPS 齐全,否则视为失败并回调 `INIT_FAILED`。
- `initRtsp` 启动 `LiveRtspServer` 时把缓存的 VPS/SPS/PPS 传入,由
`H264VideoRTPSink::createNew(...)` / `H265VideoRTPSink::createNew(...)` 生成
SDP 中的 `sprop-parameter-sets`,播放器无需等待带内参数集即可建流。
- 首帧关键帧到达前(`_is_waiting_for_idr`)不对外发流;IDR 到达后才把
`[VPS][SPS][PPS][IDR...]`(H.265)或 `[SPS][PPS][IDR...]`(H.264)打包入队,
保证新客户端拿到的描述与码流匹配。
### 3. 帧队列
`FrameQueue`(`util/frame_queue.hpp`)是有界环形缓冲(`kMaxQueueFrames = 60`,约 2 秒 @30fps),
本身是进程内单例,`push()` / `pop()` 由互斥锁保护:
- 生产者 `push()` 超限时 `pop_front()` 丢弃最老帧,防止内存增长;
- 每个消费者持有 `Reader{next_index, waiting_for_keyframe}` 游标,`pop()` 按全局序号 `_total_pushed`
取帧,多客户端互不干扰;
- 消费者落后到缓存范围之外时,先跳到最新一帧,再回扫定位到缓存中的**第一个**关键帧;
- `on_client_attach()` 从后往前扫描,让新接入的客户端从缓存中**最近**的关键帧起播;
缓存中没有关键帧时回退到最新一帧,降低首屏等待。
- 入队时 `is_key_frame` 由 `write_video_frame` 依据 NALU 类型判定(IDR),双重保险。
### 4. 时间戳
PTS **不做归一化折算**:Java 侧传入的 `ptsUs`(微秒,建议取自
`MediaCodec.BufferInfo.presentationTimeUs`,
与系统时钟同源)直接存入 `VideoFrame`;`LiveFramedSource::deliver_from_queue()` 把 `pts_us`
拆成 `timeval` 写入 `fPresentationTime`,并按 `fps` 设置 `fDurationInMicroseconds`,同一帧的全部 NAL
共用同一 pts:
```cpp
fPresentationTime.tv_sec = static_cast(_pending_pts_us / 1000000);
fPresentationTime.tv_usec = static_cast(_pending_pts_us % 1000000);
fDurationInMicroseconds = static_cast(1000000 / _fps);
```
### 5. JNI 边界
`GetDirectBufferCapacity()` 返回的是 **底层 buffer 总容量**(通常远大于一帧),
因此 Java 侧必须传入 `MediaCodec.BufferInfo.size` 作为 `size`,
native 仅 `GetDirectBufferAddress` 取地址后按 `size` 拷贝到 `std::vector`,
并对 `buffer == null` / `src == null` / `size <= 4` 三类情况直接丢弃。
6 个 native 方法:`registerStatusCallback`、`setVideoCodec`、`setVideoParameterSets`、`initRtsp`、
`pushVideoFrameBuffer`、`release`。
### 6. 停止流程
`LiveRtspServer::stop()` 先注销 `FrameQueue` 的「有新帧」唤醒回调,再经
`triggerEvent(_wakeup_trigger_id)` 唤醒阻塞在 `doEventLoop` 的线程,由 `handle_wakeup_event`
置位 `_loop_watch` 使循环退出,再 join 线程并逐层释放(trigger → ServerMediaSession → RTSPServer
→ Environment / TaskScheduler),无残留线程。
新帧唤醒使用独立的 `_frame_trigger_id`,与退出 trigger 分离;`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` `_rtsp_port` |
| 挂载点 | `live` | `RtspManager::initialize()`(`streamName` 为空时回落) |
| 摄像头 | 后置(LENS_FACING_BACK) | `VideoEncoderConfig.Builder` |
| 编码格式 | `H264`(可选 `H265`) | `RtspPublisher.setVideoCodec()` |
| 分辨率 | `720 × 1280` | `VideoEncoderConfig.Builder` / `RtspManager` 兜底值 |
| 帧率 | `30` | `initRtsp(streamName, fps)` / `VideoEncoderConfig` |
| 码率 | `3 Mbps`(CBR) | `VideoEncoderConfig.Builder` |
| 关键帧间隔 | `1 s` | `VideoEncoderConfig.Builder` |
| B 帧 | `0`(API 29+) | `VideoEncoder.createVideoFormat()` |
| 色彩 | BT709 / SDR / LIMITED | `VideoEncoder.createVideoFormat()` |
| 编码超时 | `10 ms` | `VideoEncoder.TIMEOUT_US`(`dequeueOutputBuffer`) |
| 帧队列容量 | `60` 帧(约 2s @30fps) | `util/frame_queue.hpp` `kMaxQueueFrames` |
| 取帧轮询间隔 | `5 ms` | `server/live_framed_source.hpp` `kRetryIntervalUs` |
| 单帧上限 | `2 MB` | `LiveFramedSource::maxFrameSize()` |
| RTP 输出缓冲 | `2 MB` | `LiveRtspServer::start()` `OutPacketBuffer::increaseMaxSizeTo`(HEVC 大 IDR 需要) |
| SDP 估算码率 | `5000 kbps` | `LiveServerMediaSession::createNewStreamSource()` |
| 会话描述 | `rtsp stream` | `LiveRtspServer::start()`(`ServerMediaSession` info 字段) |
---
## 注意事项
- **必须在子线程消费编码数据**:`FrameDataCallback` 与 MediaCodec 输出缓冲区共享内存,回调返回后数据可能被覆盖;本项目的
`pushVideoFrameBuffer` 内部已做拷贝到 `std::vector`。
- **`size` 一定用 `info.size`**:不要用 `buffer.remaining()` 之外的容量值,更不要把 `capacity` 当长度。
- **先注册回调再 `initRtsp`**,否则拿不到 `RTSP_SERVER_STARTED` 中的可播放 URL。
- **先 `setVideoParameterSets` 成功再 `initRtsp`**:参数集缓存为空时 `initRtsp` 启动失败,回调
`RTSP_SERVER_STOPPED`。
- **`release()` 幂等**:Activity `onDestroy` 中调用即可,重复调用安全,但每次都会回调一次 `RELEASED`。
- **端口占用**:`8554` 被占用时 `RTSPServer::createNew()` 返回 `nullptr`,会回调
`RTSP_SERVER_STOPPED`;
此时 `_init_failure_notified` 置位,只有下次重建成功后才会清除,**不会每帧重复回调**。
- **超大 IDR**:单帧超过 `fMaxSize`(2 MB)会被丢弃,播放器需等待下一个 IDR;高码率/高分辨率场景可适当调大
`maxFrameSize()`。HEVC 的 IDR 单 NALU 可达几百 KB,已在 `LiveRtspServer::start()` 中将
`OutPacketBuffer` 放大到 2 MB,避免被 framer 输入缓冲截断。
- **网络**:RTSP 默认走 UDP,弱网建议播放器显式指定 `rtsp_transport tcp`。
- **`STREAM_FAILED` 当前不会触发**:native 侧预留了该状态码,但没有任何代码路径发出 `StreamFailed`
,不要依赖它做故障恢复。
- **`consumer-rules.pro` / `proguard-rules.pro`**:当前两个 library 模块的 `minifyEnabled` 均为
`false`,发布前若开启混淆需自行补齐 native 方法与回调接口的 keep 规则。
---
## License
见 [LICENSE](LICENSE)。