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