# RtspPusher **Repository Path**: AndroidCoderPeng/RtspPusher ## Basic Information - **Project Name**: RtspPusher - **Description**: Android 平台纯视频 RTSP 推流工程:Camera2 采集 → OpenGL 渲染(水印/时间戳)→ MediaCodec 硬编码 H.264 → JNI 直传 → rtsp muxer → RTSP/TCP·UDP 推流。 - **Primary Language**: Kotlin - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 5 - **Forks**: 1 - **Created**: 2025-12-13 - **Last Updated**: 2026-09-02 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # RtspPusher Android 平台 **纯视频 RTSP 推流**工程:**Camera2 采集 → OpenGL 渲染(水印/时间戳)→ MediaCodec 硬编码 H.264 → JNI 直传 → FFmpeg `rtsp` muxer → RTSP/TCP·UDP 推流**。 - 采集/编码全部使用系统硬编码,不做软编 - 视频帧通过 `MediaCodec` 输出缓冲区的 **直接 ByteBuffer** 传到 Native,避免 `byte[]` 的 Java 堆分配与 JNI 数组访问开销 - FFmpeg 只负责 RTP/RTSP 封装与发包;裁剪后仅依赖 `libavcodec` / `libavformat` / `libavutil` 三个库 - 传输方式(TCP/UDP)运行时可切换,默认 TCP - 不含音频:监控安防与低延迟场景下纯视频推流是最常见的实践 --- ## 一、工程结构 ``` RtspPusher ├── app/ Demo 应用 │ └── com.pengxh.app.rtsp │ ├── PusherApplication 初始化 SaveKeyValues(SharedPreferences 封装) │ ├── PermissionActivity EasyPermissions 申请 CAMERA,通过后才进主界面 │ ├── MainActivity 预览 + 水印 + 编码 + 推流主逻辑 │ ├── PlatformConfigDialog 收流地址配置弹窗(IP / 端口 / 一级路径 / 二级路径) │ └── StreamConfig 地址拼装、校验与本地持久化(Gson + SP) │ ├── encoder/ 采集 + 编码库 │ └── com.pengxh.encoder │ ├── VideoEncoder Camera2 + GL + MediaCodec 核心引擎 │ ├── VideoEncoderConfig 编码参数(不可变 + Builder) │ ├── H264DataCallback 编码数据回调接口(ByteBuffer 直传) │ ├── EncoderState 编码器状态枚举 │ ├── SizeSelector 相机输出尺寸选择(宽高比匹配 + 面积最近) │ └── gl/ │ ├── EglCore EGL 环境封装 │ ├── EglRenderLayer GL 渲染层(对外提供相机输入 / 编码输出 Surface) │ ├── FrameEglRenderer 帧渲染器(双路输出:预览 + 编码) │ ├── EglRenderFilter 滤镜接口 │ ├── PassThroughFilter OES → FBO 旋转直通 │ ├── WatermarkFilter 水印滤镜(公司 / 位置 / 时间) │ ├── WatermarkController 水印控制器(线程安全,post 到 GL 线程执行) │ ├── WatermarkConfig 水印配置(Builder,enabled 默认 false) │ ├── ShaderProgram 着色器编译工具 │ └── FloatArray 顶点 / 纹理坐标工具 │ ├── pusher/ JNI 封装 + FFmpeg RTSP 推流库 │ └── com.pengxh.media │ ├── RtspPusher JNI 声明(object 单例)+ 库加载 │ ├── RtspMode TCP / UDP 传输模式枚举 │ ├── RtspStatus 推流状态码常量 │ └── RtspStatusCallback 状态回调接口 │ └── build_ffmpeg.sh FFmpeg 交叉编译脚本 ``` | 模块 | 包名 | 说明 | |-----------|-----------------------|------------------------------------------------------------------------------------------------------| | `app` | `com.pengxh.app.rtsp` | 权限申请、预览、水印开关、传输模式切换、地址配置、推流控制 | | `encoder` | `com.pengxh.encoder` | `VideoEncoder`(Camera2 + GL + MediaCodec H.264)、`SizeSelector`(尺寸选择)、`gl/` 下的渲染与水印 | | `pusher` | `com.pengxh.media` | `RtspPusher.kt`(JNI 声明)、`RtspMode` / `RtspStatus` / `RtspStatusCallback`,`jniLibs` 下为预编译 FFmpeg(3 个) | ### Native 源码结构 ``` CMakeLists.txt 三个 IMPORTED .so + 自有源码编译 RtspPusher.cpp JNI 入口(6 个 native 方法) src/ ├── global_definition.hpp RtspStatus / RtspMode 枚举(与 Java 侧一一对应) ├── java_callback.{hpp,cpp} 回调透传(AttachCurrentThread + 全局引用) ├── rtsp/ │ └── rtsp_manager.{hpp,cpp} 核心:muxer 生命周期、PTS 归一化、写帧 └── util/ ├── h264_splitter.{hpp,cpp} 格式检测 / AVCC→AnnexB / SPS·PPS 提取 / slice 检测 └── logger.{hpp,cpp} 日志封装(d_box 方框输出) libavcodec/ libavformat/ libavutil/ FFmpeg n4.4 头文件 jniLibs/${ANDROID_ABI}/ libavcodec.so / libavformat.so / libavutil.so ``` `RtspManager` 为进程内单例(`RtspManager::get()`),禁止拷贝。 头文件 include guard 统一采用 `RTSPPUSHER_` + 模块根起的文件路径大写(分隔符转 `_`)+ 结尾 `_`,例如 `pusher/src/main/cpp/src/rtsp/rtsp_manager.hpp` 对应 `RTSPPUSHER_PUSHER_SRC_MAIN_CPP_SRC_RTSP_RTSP_MANAGER_HPP_`。 --- ## 二、数据流 ``` Camera2 ──► SurfaceTexture(OES) │ ▼ OpenGL 渲染管线(PassThrough → 水印、时间戳) │ 双路输出 ├─► TextureView(本地预览) └─► MediaCodec InputSurface(COLOR_FormatSurface,硬编 H.264) │ AVCC / AnnexB ▼ H264DataCallback(ByteBuffer, size, ptsUs, isKeyFrame) │ JNI(GetDirectBufferAddress + 单次 memcpy) ▼ RtspManager::dispatch_frame → FFmpeg rtsp muxer → RTSP/TCP·UDP ``` ### Native 侧处理链(`pusher`) 1. **格式归一** `dispatch_frame()`:`h264_splitter::detect_format()` 自动识别 AVCC / AnnexB;AVCC 由 `avcc_to_annex_b()` 统一转 AnnexB,无法识别的格式直接丢弃。 2. **延迟初始化 muxer** `handle_video_frame()`:未初始化时用 `h264_splitter::extract_sps_pps()` 提取 **SPS(7) / PPS(8)**;两者齐备才调用 `update_ffmpeg_config()` 建流、写 `extradata`、 `avformat_write_header()`。 > 因此**首个关键帧必须携带 SPS/PPS**;连续 10 帧仍拿不到则回调 `STREAM_FAILED`(仅通知一次)并自动 > `release()`。 3. **纯参数集帧过滤**:`h264_splitter::contains_slice()` 判定帧内是否含 slice;仅含 SPS/PPS/SEI 的帧**只用于初始化 muxer,不入流**。 4. **PTS 归一化** `send_video_frame()`:以首帧 PTS 为基准归零,并保证严格单调(重复/倒退时 `+1` tick)。 5. **时间基转换**:`av_rescale_q()` 把微秒换算到视频 `1/90000` 时基;`duration` 按 `1000000/fps` 换算。 6. **写帧**:`av_write_frame()`(单流无需 interleaved),`AVPacket` 每次分配、写完立即释放。 7. **状态回调**:`JavaCallback` 通过 `AttachCurrentThread` 把状态码 + 消息透传回 Java 层 `RtspStatusCallback`。 `initialize()` / `update_ffmpeg_config()` / `send_video_frame()` / `release()` 均由 `_ffmpeg_config_mutex` 保护,可从编码线程直接调用。 --- ## 三、环境要求 | 项 | 版本 | |-------------------------|----------------------------------------------------------| | `minSdk` / `compileSdk` | 26(Android 8.0) / 36 | | `targetSdk` | 36 | | AGP / Kotlin | 8.11.1 / 2.3.20 | | NDK / CMake / C++ | 21.4.7075529 / 3.22.1 / C++14 | | JDK | 11(`sourceCompatibility` = `targetCompatibility` = 11) | | ABI | `armeabi-v7a`、`arm64-v8a`、`x86`、`x86_64` | | FFmpeg | 见 `build_ffmpeg.sh`,基于 `android-ndk-r21e`,API 26,产出 3 个库 | 主要三方依赖: | 模块 | 依赖 | |-----------|-----------------------------------------------------------------------------------------------------------------------------------| | `app` | `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` | | `pusher` | `androidx.core:core-ktx:1.17.0` | 所需权限(见 app 模块 `AndroidManifest.xml`): - `CAMERA`(必须,缺失时 `VideoEncoder` 抛 `SecurityException`) - `INTERNET`、`ACCESS_NETWORK_STATE`、`ACCESS_WIFI_STATE`、`BACKGROUND_CAMERA` --- ## 四、快速接入 ### 1. 注册状态回调(必须在 `initRtsp` 之前) ```kotlin RtspPusher.registerStatusCallback(object : RtspStatusCallback { override fun onStatusChanged(code: Int, message: String) { when (code) { RtspStatus.INIT_SUCCESS -> videoEncoder?.setEncodingEnable(true) RtspStatus.INIT_FAILED, RtspStatus.STREAM_FAILED -> videoEncoder?.setEncodingEnable(false) RtspStatus.STREAM_STARTED -> { /* SDP 已发出,真正开始推流 */ } RtspStatus.RELEASED -> { /* 已释放 */ } } } }) ``` ### 2. 选择传输模式(可选,默认 TCP) ```kotlin RtspPusher.setRtspMode(RtspMode.TCP) // 或 RtspMode.UDP ``` - 可随时调用,**下一次推流时生效**;推流过程中切换不会重建当前连接 - 传入非法值时 Native 会回退到 TCP 并打印告警 - `initRtsp()` 也可直接传 `mode`,效果与先 `setRtspMode()` 再 `initRtsp()` 一致 ### 3. 采集与编码 ```kotlin val config = VideoEncoderConfig.Builder() .setCameraFacing(CameraCharacteristics.LENS_FACING_BACK) .setWidth(720) .setHeight(1280) .setFrameRate(30) .setBitrate(3_000_000) .setIFrameInterval(1) .build() val watermarkFilter = WatermarkFilter().apply { setCompany("公司名称") setLocation("位置信息") setShowTime(true) } videoEncoder = VideoEncoder( this, config, binding.textureView, watermarkFilter, object : H264DataCallback { override fun onFrameEncoded( buffer: ByteBuffer, size: Int, ptsUs: Long, isKeyFrame: Boolean ) { RtspPusher.pushVideoFrameBuffer(buffer, size, ptsUs, isKeyFrame) } override fun onEncoderError(e: Exception) { e.printStackTrace() } }).apply { start() } // 水印开关(线程安全,可在任意线程调用) watermarkController = videoEncoder?.watermarkController watermarkController?.setEnabled(true) ``` 主要方法: | 方法 | 说明 | |------------------------------|-------------------------------------------------------------------| | `start()` | 启动 GL 层与相机,进入预览;已 `release()` 则直接返回 | | `setEncodingEnable(Boolean)` | 动态启停编码。不重建 `CameraDevice`,但**每次都会重建 `CaptureSession`**;已处于目标状态时跳过 | | `getEncoderState()` | 返回 `EncoderState`:`IDLE/STARTING/ENCODING/STOPPING` | | `getWatermarkController()` | 获取水印控制器(`setEnabled`/`setCompany`/`setLocation`/`setShowTime`) | | `release()` | 幂等释放:停编码、关 session、关相机、退出编码线程、释放 GL | > 编码输出在独立线程(`startDrainOutput()`)中以 `TIMEOUT_US = 10ms` 轮询 `dequeueOutputBuffer`, > `onFrameEncoded` 因此**在编码线程回调**,不在主线程。 ### 4. 开始 / 停止推流 ```kotlin RtspPusher.initRtsp("rtsp://192.168.1.100:554/camera/live", 720, 1280, 30, 3_000_000, RtspMode.TCP) // ... RtspPusher.release() ``` 完整串联顺序见 `MainActivity`: ``` registerStatusCallback → VideoEncoder.start()(先预览) → 点击推流:StreamConfig.load() 校验收流地址 → initRtsp(url, ..., mode) → [INIT_SUCCESS] → setEncodingEnable(true) → 首帧带 SPS/PPS → 自动初始化 muxer → [STREAM_STARTED] → 按钮变为"停止推流" ``` > Demo 中点击"开始推流"后立即把按钮置灰,直到收到 `INIT_FAILED` / `STREAM_STARTED` / > `STREAM_FAILED` / `RELEASED` 才恢复;点击"停止推流"则**立即**置回可点击(`release()` 与按钮状态 > 同步执行,不等待 `RELEASED` 回调)。 ### 5. 收流地址配置(Demo) `PlatformConfigDialog` 收集 **IP / 端口 / 一级路径 / 二级路径**,`StreamConfig` 负责: - `normalizePath()`:去掉空白与多余斜杠,`/pad` → `pad` - `create()`:自动剥离 `rtsp://` 前缀与结尾 `/` - `isValid`:IP 非空、端口 1~65535、二级路径非空 - `url`:拼成 `rtsp://://`,一级路径为空则自动省略 - `save()` / `load()`:Gson 序列化后存 `SharedPreferences` 未配置地址时点击推流会提示"请先配置收流地址"并直接返回。 --- ## 五、JNI 接口(`com.pengxh.media.RtspPusher`) | 方法 | 说明 | |---------------------------------------------------------|--------------------------------------| | `registerStatusCallback(cb)` | 注册推流状态回调,需在 `initRtsp` 前调用 | | `setRtspMode(mode)` | 设置传输模式 `RtspMode.TCP` / `UDP`,下次推流生效 | | `initRtsp(url, width, height, fps, bitrate, mode)` | 创建 RTSP 输出上下文(此时还未建流、未写 SDP) | | `pushVideoFrameBuffer(buffer, size, ptsUs, isKeyFrame)` | 推视频帧(推荐,直传 `ByteBuffer`) | | `pushVideoFrameBytes(bytes, ptsUs, isKeyFrame)` | 推视频帧(已废弃,多一次 JNI 数组拷贝与 Java 堆分配) | | `release()` | 写 trailer、释放资源并回调 `RELEASED` | ### 传输模式(`RtspMode`) | 常量 | 值 | 含义 | |-------|---|----------------------------------| | `TCP` | 0 | `rtsp_transport=tcp`,抗丢包、易穿透 NAT | | `UDP` | 1 | `rtsp_transport=udp`,延迟更低 | 默认值与 Demo 界面初始勾选一致,为 `TCP`。 ### 状态码(`RtspStatus`) | 常量 | 值 | 含义 | |------------------|---|-------------------------------------------| | `INIT_SUCCESS` | 0 | `initRtsp` 成功,可以开始编码 | | `INIT_FAILED` | 1 | `initRtsp` 失败或 `avformat_write_header` 失败 | | `STREAM_STARTED` | 2 | 写头成功,SDP 已发出,开始推流 | | `STREAM_FAILED` | 3 | 连续多帧未收到 SPS/PPS 等异常,已自动释放 | | `RELEASED` | 4 | 已释放 | 库加载顺序(`RtspPusher.kt` 的 `init` 块):`avcodec → avformat → avutil → RtspPusher`。 --- ## 六、默认参数 **编码**(`VideoEncoderConfig.Builder`) | 参数 | 默认值 | |----------|----------------------------------------------------------------------------------------| | 摄像头 | `LENS_FACING_BACK` | | 分辨率 / 帧率 | 720×1280 @ 30fps | | 码率 / 模式 | 3 Mbps / CBR | | 关键帧间隔 | 1s(`-1` 表示仅首帧) | | 其他 | `COLOR_FormatSurface`、`KEY_MAX_B_FRAMES=0`(API 29+)、BT.709 + SDR Video + Limited Range | > `setWidth()` / `setHeight()` 要求正偶数,`setFrameRate()` / `setBitrate()` 要求 `> 0`, > `setIFrameInterval()` 要求 `>= -1`,否则抛 `IllegalArgumentException`。 **相机尺寸选择**(`SizeSelector.choosePreviewSize()`) 按与目标宽高比**精确匹配**(容差 `0.001`,允许宽高互换)过滤,取**面积差最小**的候选; 无任何比例匹配时兜底为"保持方向一致 + 面积最接近"。返回 `Pair`, `second == true` 表示需要宽高对调。 **RTSP**(`rtsp_manager.hpp` 成员默认值) | 字段 | 默认值 | |-----------------------|-------------| | `_frame_width/height` | 720 / 1280 | | `_fps` / `_bitrate` | 30 / 3 Mbps | | `_rtsp_mode` | `TCP` | 写头选项(`update_ffmpeg_config()`):`muxdelay=0`、`muxpreload=0`、 `rtsp_transport=`;视频流时基 `1/90000`,`avg_frame_rate = fps/1`。 --- ## 七、FFmpeg 编译 ```bash # 修改脚本顶部配置 NDK_DIR="/home/vm/android-ndk-r21e" ANDROID_API=26 ./build_ffmpeg.sh ``` - 在 Linux 下执行,循环编译 `armeabi-v7a`、`arm64-v8a`、`x86`、`x86_64` 四个架构 - 关键裁剪:`--disable-everything` 后仅开启 `muxer=rtsp`、`protocol=rtsp,tcp,udp,rtp,file`、 `parser=h264`、`bsf=h264_mp4toannexb`、`demuxer=rtsp,h264,mp4`、`decoder=h264` 及 `swscale`, 并显式 `--enable-network` > H.264 由 MediaCodec 编码,FFmpeg **不需要**任何编码器/音频组件,只需要封装与 RTP 打包能力 - 同时 `--disable-avdevice`、`--disable-postproc`、`--disable-doc`、禁用三个命令行工具与 `--disable-symver` - 纯视频场景已移除 `pcm_alaw` 编解码器、`aresample` 过滤器与 `swresample` 模块,最终只产出 `libavcodec.so`、`libavformat.so`、`libavutil.so` 三个库 - 产物 `android/${ARCH}/lib/*.so` 拷贝到 `pusher/src/main/jniLibs/${ANDROID_ABI}/`,对应的三个头文件目录 放到 `pusher/src/main/cpp/`,CMake 通过 `IMPORTED_LOCATION` 引用;`RtspPusher.kt` 中按 `avcodec → avformat → avutil → RtspPusher` 的顺序 `System.loadLibrary()` > 增删库时需**同步修改** `build_ffmpeg.sh`、`jniLibs`、`CMakeLists.txt` 与 `RtspPusher.kt`四处,否则会加载失败 --- ## 八、踩坑与注意事项 1. **H.264 必须整帧传入且带起始码**:Native 侧按 AnnexB 解析并整帧交给 FFmpeg,不能按起始码拆成多个 NALU 分别写,否则对端无画面。 2. **`size` 必须是 `MediaCodec.BufferInfo.size`**:`getOutputBuffer()` 返回的 `Buffer`,`capacity()` 是 codec 申请的总容量(远大于一帧),把 `capacity` 当长度会把 padding / 旧帧数据一起推进 muxer, 导致解析失败。JNI 侧另有一道防线:`size <= 4` 直接丢弃。 3. **回调内必须同步处理数据**:`onFrameEncoded` 的 `ByteBuffer` 与 MediaCodec 输出缓冲共享内存,回调 返回后数据可能被覆盖。Native 侧会立即 `memcpy` 到本地 `std::vector`,因此只需保证调用期间不被复用。 4. **首帧必须是含 SPS/PPS 的关键帧**:未初始化前的帧会被丢弃;`mCodec.start()` 后已主动请求 `PARAMETER_KEY_REQUEST_SYNC_FRAME`,`BUFFER_FLAG_CODEC_CONFIG` 的配置帧也会以关键帧形式上抛。 5. **纯参数集帧不会入流**:只含 SPS/PPS/SEI 而无 slice 的帧仅用于初始化 muxer,不会写进码流。 6. **时间戳统一为微秒**:PTS 归一化与时基换算都在 Native 内部完成,业务层无需关心 90kHz 时钟域。 7. **启停编码会重建 `CaptureSession`**:`setEncodingEnable()` 只保留 `CameraDevice` 与 `MediaCodec`, 但每次调用都会重建会话并走一次 `onConfigured` 回调,频繁启停会有短暂预览停顿,业务层应做好防抖。 8. **传输方式**: - UDP 延迟更低但丢包会花屏;TCP 更稳,适合弱网 / 跨网段 / 需穿透 NAT 的场景 - 若服务器要求强制 TCP,可额外设置 `rtsp_flags=prefer_tcp` - 推流侧模式与**拉流侧**的 `-rtsp_transport` 相互独立,服务器会重新分发,两者可以不一致 9. **权限**:缺少 `CAMERA` 时 `VideoEncoder` 直接抛 `SecurityException`;Demo 在 `PermissionActivity` 中拦在进主界面之前。 10. **生命周期**:`onDestroy` 中需依次调用 `RtspPusher.release()` 与 `videoEncoder?.release()`,两者均幂等。 11. **`release()` 后不能复用同一上下文**:`initialize()` 检测到 `_av_fmt_ctx_ptr` 非空会直接告警返回, 需重新 `initRtsp()`。 --- ## 九、低延迟拉流命令 ```shell ffplay -fflags nobuffer -flags low_delay -framedrop -analyzeduration 0 -probesize 32 -rtsp_transport tcp rtsp://192.168.137.1:554/camera/live ``` 参数说明:`nobuffer` + `low_delay` 关闭接收缓冲、`analyzeduration 0` + `probesize 32` 跳过码流探测(首帧秒开)、 `framedrop` 丢帧追帧;`-rtsp_transport tcp` 指定**拉流侧**与服务器的传输方式,与推流侧的 `rtsp_transport`(由 `RtspMode` 决定)相互独立,服务器会重新分发,两者可以不一致。若拉流侧也走 UDP, 把该参数改为 `udp` 即可。 --- ## 十、License 见 [LICENSE](./LICENSE)。