# 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)。