# RtspPusher **Repository Path**: AndroidCoderPeng/RtspPusher ## Basic Information - **Project Name**: RtspPusher - **Description**: RTSP协议推流 - **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-01 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # RtspPusher Android 平台 RTSP 推流工程:**Camera2 采集 → OpenGL 渲染(水印/时间戳)→ MediaCodec 硬编码(H.264 + AAC)→ JNI 直传 → FFmpeg `rtsp` muxer → RTSP/TCP 推流**。 - 采集/编码全部使用系统硬编码,不做软编 - 视频帧通过 `MediaCodec` 输出缓冲区的 **直接 ByteBuffer** 传递到 Native,避免 `byte[]` 的额外拷贝 - FFmpeg 只负责 RTP/RTSP 封装与发包(`--disable-everything` 后的极小裁剪构建) --- ## 一、工程结构 ``` RtspPusher ├── app/ Demo 应用(权限申请、预览、推流控制) ├── encoder/ 采集 + 编码库(Camera2 / OpenGL / MediaCodec) ├── pusher/ JNI 封装 + FFmpeg RTSP 推流库(含预编译 .so) └── build_ffmpeg.sh FFmpeg 交叉编译脚本 ``` | 模块 | 说明 | |-----------|----------------------------------------------------------------------------------------------| | `app` | 示例:`PermissionActivity` 申请权限,`MainActivity` 串联预览、水印、编码、推流 | | `encoder` | `VideoEncoder`(Camera2 + GL + MediaCodec H.264)、`AudioEncoder`(AudioRecord + MediaCodec AAC) | | `pusher` | `RtspPusher.kt`(JNI 声明)+ `RtspManager`(FFmpeg 封装/推流),`jniLibs` 下为预编译 FFmpeg | --- ## 二、数据流 ### 视频 ``` Camera2 ──► SurfaceTexture(OES) │ ▼ OpenGL 渲染管线(PassThrough / 水印、时间戳) │ 双路输出 ├─► TextureView(本地预览) └─► MediaCodec InputSurface(COLOR_FormatSurface,硬编 H.264) │ AVCC / AnnexB ▼ H264DataCallback(ByteBuffer, size, ptsUs, isKeyFrame) │ JNI(零额外拷贝) ▼ RtspManager → FFmpeg rtsp muxer → RTSP/TCP ``` ### 音频 ``` AudioRecord(44.1kHz / 单声道 / 16bit PCM) │ 生产者线程:按 1024 样本/帧投递队列 ▼ MediaCodec(AAC-LC, 64kbps) 消费者线程:喂 PCM + drain 输出 │ ├─► AudioSpecificConfig(2 字节 ASC,用于 SDP 的 config=) └─► AAC 帧 → pushAudioEncodedFrame → RtspManager ``` ### Native 侧关键处理(`pusher`) 1. **格式归一**:`h264_splitter::detect_format()` 自动识别 AVCC / AnnexB;AVCC 由 `avcc_to_annex_b()` 统一转 AnnexB。 2. **延迟初始化 muxer**:未初始化时,对帧做 NALU 切分,提取 **SPS(7) / PPS(8)**;两者齐备才调用 `update_ffmpeg_config()` 建流、写 `extradata`、`avformat_write_header()`。 > 因此**首个关键帧必须携带 SPS/PPS**,否则之前的帧会被丢弃等待。 3. **时间戳转换**:`av_rescale_q()` 把微秒时间戳分别换算到视频 `1/90000`、音频 `1/44100` 时基。 4. **交错写帧**:音视频均走 `av_interleaved_write_frame()`,配合 `sync=1` 做音画同步。 5. **状态回调**:`JavaCallback` 把状态码 + 消息透传回 Java 层 `RtspStatusCallback`。 --- ## 三、环境要求 | 项 | 版本 | |-------------------------|--------------------------------------------------| | `minSdk` / `compileSdk` | 26(Android 8.0) / 36 | | AGP / Kotlin | 8.11.1 / 2.3.20 | | NDK / CMake / C++ | 21.4.7075529 / 3.22.1 / C++14 | | ABI | `armeabi-v7a`、`arm64-v8a`、`x86`、`x86_64` | | FFmpeg | 见 `build_ffmpeg.sh`,基于 `android-ndk-r21e`,API 26 | 所需权限:`CAMERA`、`RECORD_AUDIO`、`INTERNET`(`AndroidManifest.xml` 已声明,运行时由 `PermissionActivity` 动态申请)。 --- ## 四、快速接入 ### 1. 注册状态回调(必须在 `initRtsp` 之前) ```kotlin RtspPusher.registerStatusCallback(object : RtspStatusCallback { override fun onStatusChanged(code: Int, message: String) { when (code) { RtspStatus.INIT_SUCCESS -> { /* 可以开始编码与采集 */ } RtspStatus.STREAM_STARTED -> { /* SDP 已发出,真正开始推流 */ } RtspStatus.RELEASED -> { /* 已释放 */ } } } }) ``` ### 2. 视频采集与编码 ```kotlin val config = VideoEncoderConfig.Builder() .setCameraFacing(CameraCharacteristics.LENS_FACING_BACK) .setWidth(720) .setHeight(1280) .setFrameRate(25) .setBitrate(2_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) ``` ### 3. 音频采集与编码 ```kotlin val audioEncoder = AudioEncoder( this, AudioEncoderConfig.Builder().build(), object : AudioDataCallback { override fun onAudioSpecificConfig(asc: ByteArray) { RtspPusher.setAudioSpecificConfig(asc) // 必须在写头之前 } override fun onFrameEncoded(buffer: ByteBuffer, size: Int, ptsUs: Long) { RtspPusher.pushAudioEncodedFrame(buffer, size, ptsUs) } override fun onError(e: Exception) { e.printStackTrace() } }) audioEncoder.start() ``` ### 4. 开始 / 停止推流 ```kotlin RtspPusher.initRtsp("rtsp://192.168.1.100:554/rtsp/live", 720, 1280, 25, 2_000_000) // ... RtspPusher.release() // 幂等:写 trailer + 释放上下文 ``` 完整串联顺序见 `MainActivity`: ``` registerStatusCallback → VideoEncoder.start()(先预览) → initRtsp() → [INIT_SUCCESS] → setEncodingEnable(true) + startRecording() → 首帧带 SPS/PPS → 自动初始化 muxer → [STREAM_STARTED] ``` --- ## 五、JNI 接口(`com.pengxh.media.RtspPusher`) | 方法 | 说明 | |---------------------------------------------------------|-------------------------------------------------| | `registerStatusCallback(cb)` | 注册推流状态回调,需在 `initRtsp` 前调用 | | `initRtsp(url, width, height, fps, bitrate)` | 创建 RTSP 输出上下文(此时还未建流) | | `setAudioSpecificConfig(asc)` | 传入 AAC 的 `AudioSpecificConfig`,写入 SDP `config=` | | `pushVideoFrameBuffer(buffer, size, ptsUs, isKeyFrame)` | 推视频帧(推荐,直传 `ByteBuffer`) | | `pushVideoFrameBytes(bytes, ptsUs, isKeyFrame)` | 推视频帧(已废弃,多一次 JNI 拷贝) | | `pushAudioEncodedFrame(buffer, size, ptsUs)` | 推已编码的 AAC 帧 | | `release()` | 写 trailer、释放资源并回调 `RELEASED` | ### 状态码(`RtspStatus`) | 常量 | 值 | 含义 | |---------------------------------------------|-------|--------------------| | `INIT_SUCCESS` / `INIT_FAILED` | 0 / 1 | `initRtsp` 成功 / 失败 | | `STREAM_STARTED` / `STREAM_FAILED` | 2 / 3 | 写头成功开始推流 / 写头失败 | | `WRITE_VIDEO_FAILED` / `WRITE_AUDIO_FAILED` | 4 / 5 | 视频 / 音频写帧失败 | | `RELEASED` | 6 | 已释放 | --- ## 六、默认编码参数 **视频**(`VideoEncoderConfig`) | 参数 | 默认值 | |----------|-------------------------------------------------------------------| | 分辨率 / 帧率 | 720×1280 @ 30fps(Demo 用 25fps) | | 码率 / 模式 | 2 Mbps / CBR | | 关键帧间隔 | 1s | | 其他 | `COLOR_FormatSurface`、`KEY_MAX_B_FRAMES=0`、BT.709 + Limited Range | **音频**(`AudioEncoderConfig`) | 参数 | 默认值 | |---------------|---------------------------| | 采样率 / 声道 / 位深 | 44100Hz / 单声道 / 16bit | | 每帧样本 | 1024(AAC-LC 标准帧,约 23.2ms) | | 码率 / Profile | 64 kbps / AAC-LC | **RTSP 参数**(`rtsp_manager.cpp`) `rtsp_transport=tcp`、`rtsp_flags=prefer_tcp`、`muxdelay=0`、`muxpreload=0`、`stimeout=5000000`、开启 reconnect 与 `sync=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`、`filter=aresample` 及 swresample/swscale > H.264 与 AAC 均由 MediaCodec 编码,FFmpeg **不需要**编码器,只需要封装与 RTP 打包能力 - 产物 `android/${ARCH}/lib/*.so` 与 `include/` 拷贝到 `pusher/src/main/jniLibs/${ANDROID_ABI}/` 与头文件目录,CMake 通过 `IMPORTED_LOCATION` 引用 --- ## 八、踩坑与注意事项 1. **H.264 数据必须整帧传入且带起始码**:Native 侧按 AnnexB 解析并整帧交给 FFmpeg,不能按起始码拆成多个 NALU 分别写,否则对端无画面。 2. **`size` 必须是 `MediaCodec.BufferInfo.size`**:`MediaCodec.getOutputBuffer()` 返回的 `Buffer`, `capacity()` 是 codec 申请的总容量(远大于一帧),直接把 `capacity` 当长度会把 padding / 旧帧数据一起推给 muxer,导致解析失败。 3. **回调内必须同步处理数据**:`onFrameEncoded` 的 `ByteBuffer` 与 MediaCodec 输出缓冲共享内存,回调返回后数据可能被覆盖。 4. **首帧必须是含 SPS/PPS 的关键帧**:未初始化前非关键帧会被丢弃;`start()` 后已主动请求 `PARAMETER_KEY_REQUEST_SYNC_FRAME`。 5. **ASC 要先于写头设置**:`setAudioSpecificConfig()` 必须在首帧触发 `avformat_write_header()`之前调用,否则 SDP 中缺少 `config=`,对端无法解码音频。 6. **音视频时钟统一**:Java 侧时间戳统一为微秒,Native 内部用时基换算,业务层无需关心 90kHz / 采样率时钟域。 7. **传输方式**:默认强制 TCP(`rtsp_transport=tcp`),如需 UDP 需修改 `rtsp_manager.cpp` 中的 `av_dict_set` 选项。 8. **权限**:缺少 `CAMERA` / `RECORD_AUDIO` 时 `VideoEncoder` / `AudioEncoder` 直接抛 `SecurityException`。 --- ## 九、已知遗留问题 - `build_ffmpeg.sh` 中仍保留 `--enable-encoder=pcm_alaw --enable-decoder=pcm_alaw`,属于早期 G.711 μ-law 方案残留;当前音频为 AAC,可移除以进一步裁剪体积。 - Demo 布局中的「本地录像」勾选框(`recordCheckBox`)尚未实现对应逻辑。 ## 十、License 见 [LICENSE](./LICENSE)。