☰
Android端RTMP直播推流:librestreaming集成与踩坑实践
2026/10/14 5:06:48 网站建设 项目流程

FFmpeg开发笔记这个系列做了这么久,大部分时间都在跟音视频的采集、编码、封装、传输打交道。在移动端做RTMP直播推流这件事,我一直习惯用FFmpeg那套体系去处理,毕竟编译脚本、转封装逻辑、协议兼容性都熟。但前阵子在给一个Android端的直播项目优化启动耗时和包体积时,发现纯FFmpeg的推流方案在某些场景下确实偏重了,初始化要跑协议栈、封装器,还得自己接摄像头预览和音频采集。后来接触到国产的librestreaming这个开源推流库,底层直接走librtmp,配合MediaCodec硬编和x264软编,在Android上做RTMP直播推流明显更轻快。这篇文章就把我集成librestreaming做RTMP直播的完整过程、参数取舍和踩坑记录整理出来,给同样在做移动端直播推流的朋友一个参考。

1. 为什么在FFmpeg笔记里聊一个非FFmpeg的推流库

1.1 移动端RTMP推流的现实选型

先说实话,这个系列讲FFmpeg,但Android端做RTMP推流,可选的路子其实就那几条:

第一是FFmpeg全家桶,把libavformat、libavcodec、libavfilter全部编进APK,用avformat_write_header、av_interleaved_write_frame这套标准流程推到RTMP服务器。优势是稳定、可控、协议栈完整,劣势是so体积大,初始化慢,而且从摄像头拿到YUV数据到喂给编码器之间,要自己搭管线。对很多只想快速出活的团队来说,前期搭建成本不低。

第二是librtmp直推。很多做了多年的直播项目,底层实际是rtmpdump那套librtmp协议栈,自己管理握手、Chunk流、FLV tag封装,再配合Android自带的MediaCodec做硬编。这条路比FFmpeg轻,但裸露程度高,要自己处理编码器输出和RTMP发送之间的缓冲、时间戳、音视频交织,一个细节没对接好就掉链子。

第三是直接用国内云厂商的直播推流SDK,省事是省事,但耦合重、收费模式五花八门,而且很多SDK号称开源,真要改底层还得看脸色。

librestreaming恰好卡在第二和第三之间。它把librtmp的RTMP传输部分封装好,把摄像头预览、音视频编码、RTMP发送打包成一套完整管线,对外暴露配置对象和Publisher对象。对开发者来说,不需要自己写FLV打包逻辑,也不用操心Rtmp握手细节,配置好URL和编码参数,调start就能推流。这正是我在这篇笔记里聊它的原因——它是FFmpeg之外一个思路恰好互补的国产开源方案。

1.2 librestreaming解决了什么问题

先说它解决的最大问题:在Android上把“采集-编码-发送”串成一条稳定管线,而不是甩给你一堆底层函数自己拼。

我用FFmpeg推流时最头疼的不是编码,而是自己写预览和编码的桥接。摄像头SurfaceTexture拿到的纹理数据、MediaCodec的输入Surface、FFmpeg编码器的AVFrame,三方怎么高效流转,说起来简单,做起来全是坑。librestreaming把Camera预览和视频编码放到一个GL线程里,用SurfaceTexture + MediaCodec的Surface输入模式,让数据在GPU里完成流转,绕过了CPU拷贝。这条路径比FFmpeg的经典做法在移动端更高效。

第二个解决的问题是硬编软编切换。Android碎片化严重,不同芯片的MediaCodec实现差异大,librestreaming做了编码器探测,硬编异常时能降级到内置的软编。这个兜底对直播场景太重要了,毕竟线上用户手里的机器参数永远超出你的预期。

第三个问题是时间戳管理。RTMP推流要求音视频时间戳同步递增,librestreaming在采集端就给每一帧打上单调时钟基准的时间戳,编码后再映射到RTMP的timestamp域。我自己用FFmpeg写推流时最容易翻车的就是这里,音视频分别从Camera和AudioRecord拿数据源,时间基准不统一,推上去就音画不同步。用这个库省掉了很多脏活。

2. librestreaming的核心架构与工作机理

2.1 从Camera到RTMP服务器的数据通路

看librestreaming的源码结构,其实跟我自己手写一套推流管线的思路非常像,只不过它把每个环节都组件化了。整体数据流是这样的:

摄像头预览帧通过SurfaceTexture进入GL环境,同时纹理被绑定到MediaCodec的输入Surface。编码器输出H.264码流,经过一个打包层按FLV规范封装成VideoTag,再交给librtmp的发送缓冲。音频侧更简单,AudioRecord拿到的PCM数据送入AAC编码器,封装成AudioTag后也进入同一条发送链路。

这条链路上有个关键点:视频编码是异步的。MediaCodec输出端要单独用dequeueOutputBuffer收数据,librestreaming里专门有个线程循环收编码器输出,另一条线程负责从发送Buffer里取数据交给网络socket。两个线程中间用队列解耦,避免网络抖动拖死编码线程。

音频侧需要注意音频编码器输出的前几帧。AAC编码器最开始会输出AudioSpecificConfig,也就是平时说的ASC信息,后面跟着的是带ADTS头或者裸的AAC帧。RTMP封装的AudioTag里,第一个tag要带ASC,后续的tag只带裸AAC帧数据。librestreaming内部处理了这一套,但如果你自己要改动编码器或者升级版本,这个细节千万别弄丢。

2.2 视频编码硬软切换的隐藏逻辑

librestreaming的视频编码抽象做得挺有意思。它通过一个编码器接口来统一硬编软编,接口的核心方法大概就是configure、start、drainEncoder这一组。硬编是MediaCodec的Surface模式,软编是x264的Android移植版本。

硬编为什么优先?这不难理解。Surface模式把编码器输入直接绑定在GPU纹理上,省掉了RGBA到YUV的CPU转换。我实测下来,720p的输入分辨率,硬编在帧率稳定性上有明显优势,功耗和发热也低一截。

但要命的是硬编的兼容性。部分老设备的MediaCodec对H.264 High Profile支持不完整,也有设备在分辨率不是16:16倍数时直接罢工。librestreaming的做法是先请求一个支持的最低配置,如果编码器初始化失败,捕获异常后自动切换到软编。这里有个实用经验:初始化阶段不能只等着看有没有异常,因为有些设备MediaCodec要等到输入第一帧数据后才开始报错。所以配置里最好主动提供一个开关,允许在硬编不可用时显式降级,别只依赖自动兜底。

2.3 音频编码和时间戳处理细节

音频部分比较简单,没那么花哨,但越简单越容易忽视时间戳。librestreaming的音频时间戳基于AudioRecord读多少字节来累加,单位换算成毫秒。举例来说,44.1kHz采样率、单声道、16bit位深,一秒钟就是88200字节。每次从AudioRecord读到8820字节(约100ms的音频)时,时间戳加100。这套逻辑是准的,前提是AudioRecord的读取速率稳定。但实际上别指望它完全匀速,系统负载高的时候,一次读出来的数据可能超过缓冲,时间戳算错就直接影响音画同步。

视频时间戳则是在每次CamTexture接收新帧的时刻取系统单调时钟,但这个时钟不能跟音频混用。最好的方式是用同一条时间基准换算,比如都用AudioTrack的播放时钟或者SystemClock.elapsedRealtime。librestreaming内部的做法是把视频帧的取帧时间和音频字节累加映射到同一个毫秒时间轴,在RTMP发送时统一换算。如果你以后要改造这个库,时间基准统一这件事一定要守住,拼音画不同步十个里九个栽在这里。

3. Android工程集成与RTMP直播落地实操

3.1 工程接入方式和权限准备

librestreaming的接入方式有两种:一种是在Gradle里直接加依赖,另一种是把源码作为一个module引入工程。作为一直习惯改源码的工程党,我更推荐第二种。原因是直播推流库跟业务强相关,你要加弱网检测、加自定义美颜、改编码参数,依赖aar反而不好下手。

源码导入之后,模块结构大概是这样的:有个包专门放RTMP核心,包括RtmpSender、RtmpPublisher、Packet等;有个包放编码器实现;还有包放Camera预览和GL渲染。导入时要注意模块的minSdkVersion设置,我记得默认要求21以上,API 21开始Android才普遍支持MediaCodec的Surface输入模式,低于这个版本硬编走不了Surface模式。

权限配置这部分,先看清单文件:

<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

MODIFY_AUDIO_SETTINGS经常有人漏掉,它涉及音频会话的调整,不加在某些机型上会出现声音采集异常。另外,Android 6.0以上还要动态申请CAMERA和RECORD_AUDIO权限,这部分逻辑写在Activity里,跟普通权限申请没有区别,就不展开了。

3.2 推流核心API与参数配置示例

librestreaming的核心使用方式,我整理了一个最小可用示例。不用管具体的类名全路径,不同的版本可能会微调,核心流程是固定的:

// 构建配置对象 RTMPPublisherConfig config = new RTMPPublisherConfig(); config.setVideoResolution(1280, 720); config.setVideoFps(15); config.setVideoBitrate(800 * 1024); config.setVideoEncoder(VideoEncoder.MEDIACODEC); // 或软编 config.setAudioSampleRate(44100); config.setAudioChannelCount(1); config.setAudioBitrate(64 * 1024); config.setAudioEncoder(AudioEncoder.MEDIACODEC); config.setRtmpUrl("rtmp://你的推流地址/live/streamKey"); config.setCameraOrientation(90); config.setVideoGopSize(30); // 创建Publisher并预览 RTMPPublisher publisher = new RTMPPublisher(); publisher.setConfig(config); publisher.setCameraPreview(glSurfaceView); publisher.setCameraFacing(CameraFacing.FRONT); publisher.start();

逻辑非常简单,start里面会做编码器初始化、RTMP握手、丢出首帧后开始推流。注意setVideoGopSize这个参数,也就是关键帧间隔,它决定一个GOP内有多少帧。GOP太大,拉流端起播等待时间长;GOP太小,同等码率下画质会变差。通常FPS的两倍是个起点。

推流停止也有讲究,直接调用stopPublisher不行,要先stopEncode再发送一个空的AVC解码器序列头或者等当前帧发送完,才能安全关socket。直接粗暴stop,服务器端收到的FLV流可能是不完整的,拉流端会出现花屏或者无法播放。

3.3 前后台切换与生命周期管理

做直播App难免遇到用户按Home键切后台。这个过程如果处理不好,会导致推流中断、黑屏或者声音断流。

librestreaming的处理原则是:切后台时,摄像头资源释放,编码器stop,但RTMP连接和socket保持。等用户回到前台,重新初始化Camera预览,重启编码器,接着上次的时间戳继续推流。但这里有个取舍。录了一段时间回来,编码器的SPS/PPS可能变了,而RTMP连接保持的话,服务器端可能还拿着老参数,导致拉流端解码失败。

我试过两种做法。一种是切后台时直接完全停推,回到前台重新起推流。缺点是RTMP重连有耗时,用户看到的画面会中断几秒。另一种是保连接但重开编码器时,发送一个新的AVC sequence header给服务器。librestreaming在某些版本里没有把这个逻辑做完整,需要自己手动补。我的建议是:如果只是切后台几十秒,保连接方案体验更好;超过一分钟的重后台,直接断掉重推更省心,也避免长时间挂在一个空连接上被服务端踢掉。

4. 直播参数调优与实操经验

4.1 分辨率、码率、帧率怎么定才合理

直播画质和流畅度的平衡,在于分辨率、码率、帧率三个参数的匹配。我在模拟项目X里实测下来的经验是,这几个参数不是独立变量,而是绑定关系。

先说分辨率。Android设备的摄像头输出可以选720p或者1080p。但注意,RTMP推流不是分辨率越高越好,还要看网络上行带宽和编码器能力。1080p需要至少3到4Mbps的码率才能维持可接受的画质;720p在2Mbps以内的码率就可以有不错的效果。对大多数观众的移动网络来说,720p比1080p更稳妥。

码率的选择直接决定观众看到的画质。我一般用一个经验公式:视频码率等于宽度乘高度乘帧率乘以一个经验系数,简单估算。比如720p、15帧,系数取0.1左右,码率约等于1280x720x15x0.1约等于1.38Mbps。实际调优时,对照组测试最实在:固定分辨率帧率,把码率从1Mbps往上加,每档推一分钟,观察画质、卡顿和主播端发热。

拉流端实际看到的画面质量还受关键帧间隔影响。把GOP时间控制在2秒以内,即30帧时GOP设为60,可以保证拉流端快速起播和切换清晰度时有参考帧。

4.2 延迟优化三板斧

直播延迟大体有三块:采集端缓冲、编码缓冲、网络缓冲。librestreaming推流端能调的,主要是网络缓冲和编码参数。

第一是关闭RTMP的TCP_NODELAY,不对,说反了,是开启TCP_NODELAY。RTMP走TCP,默认可能有Nagle算法,会把小包攒着再发,直播音视频数据本来走的是实时性优先,开启TCP_NODELAY后小包立即发送,能减少10到30毫秒的包延迟。实现方法就是在librtmp的socket上设置TCP_NODELAY选项。

第二是控制发送缓冲大小。librestreaming内部有一个发送队列,用来平滑网络抖动。但这个队列不能太长,队列越长,延迟越大。我一般把发送缓冲控制在300到500毫秒的数据量。在弱网场景下,队列稍微大一点可以避免频繁断连,但超过800毫秒观众端的体感延迟就很难接受了。

第三是音频编码延迟。AAC编码器的采样率越高,单帧编码延迟越大。44.1kHz下,一帧AAC是1024个采样点,约23ms,这个延迟可以忽略。但有些设备上AAC编码器配置不佳,会额外增加buffer。需要在编码器configure时设置合理的声道数和采样率,用默认值反而容易踩坑。

4.3 断线重连与弱网处理策略

移动网络切换、WiFi不稳定、上行带宽不足,直播推流断线是常态。librestreaming本身没有内置自动重连机制,需要自己在外层包装。

我的做法是这样的:监听推流回调里的错误码,区分是RTMP握手失败、网络超时还是服务器主动断开。前两类错误立即有效,等网络恢复后重连;服务器主动断开则要检查推流地址的鉴权和时效。

重连过程要注意推流地址的时效。很多云直播服务的推流地址带有效期,断线时间长了地址过期,重连永远失败。需要在重连时重新拉取推流地址,而不是拿缓存的地址死磕。

弱网下的另一个策略是动态降码率。librestreaming没有内置自适应码率,但我们可以通过周期检测发送缓冲的数据积压量来估算网络带宽。当缓冲积压超过阈值且持续一段时间,回调到业务层,业务层重新配置编码器码率降档。这个比事后断线重连更能保住观众体验。

5. 常见问题排查与避坑实录

5.1 推流黑屏但RTMP连接正常

这个现象在硬编模式下比较常见。表面上看,Publisher连接到了服务器,服务器也收到数据,但拉流端画面黑屏。原因多半是摄像头预览占了主线程,GL线程没初始化好,导致MediaCodec输入Surface没有数据。

排查思路是先确认摄像头是否出流。把TextureView/GLConsumer输出局部渲染到一个可看到的View,如果这里还是黑的,说明是摄像头打开或预览管线的问题。如果预览正常但推流黑屏,那就查编码器的输出码流有没有SPS/PPS帧。在RTMP推送时,第一个视频tag必须是AVC sequence header,里面带SPS/PPS。某些场景下手动重启编码器后没有重新发送这个header,服务器会一直拿不到解码信息。

还有一个小坑是设置摄像头帧率。librestreaming打开摄像头时要根据config里的FPS来设置Camera.Parameters,如果设备不支持设置的帧率,摄像头预览不回调,GL线程等不到新帧,推流自然黑屏。这时候代码里要做帧率回退,比如15帧不支持就降到10帧。

5.2 音画不同步的处理思路

音画不同步是RTMP直播里被问得最多的问题。前面提到时间基准不统一是根源,这里再说一个常见的表现:声音快、画面慢,而且差异越来越大。

音频时间戳是按采样字节累加算的,非常线性;视频时间戳是按Camera取帧时的系统时钟算的。如果系统时钟出现跳跃,或者Camera取帧线程被其他任务拖慢,视频时间戳就会落后于音频。当差距累积到几百毫秒时,观众听起来就是口型对不上。

我在自己的项目里加了一个修正策略:发送时对比当前音频和视频的时间戳差值,如果视频落后音频超过一帧的时长(比如100ms),就把视频时间戳强制对齐到音频时间轴。RTMP服务器和拉流端一般会做一定的jitter buffer校正,小幅修正不会影响播放。

5.3 RTMP握手失败或连接超时

RTMP握手失败,得一帧一帧看log。常见原因有三种:

一是推流地址的路径写错。RTMP URL是有讲究的,rtmp://domain/live/streamKey里,app是live,streamKey是流名称。有些服务商要求带鉴权参数,直接拼在streamKey后面。少了一个斜杠或者多了参数分隔符,都能导致握手失败。

二是CDN只允许指定IP或token。这个跟librestreaming本身没关系,但排查时容易被误导。

三是DNS解析缓慢。librestreaming的RTMP连接是同步阻塞的,如果域名解析慢,整个start会卡住。建议在业务层先把推流域名解析成IP再交给RTMP库,或者至少放到子线程,避免阻塞UI。

5.4 推流发热和内存上涨

移动端做长时间直播,发热和内存必须盯紧。发热的重灾区是视频编码,如果设备硬编支持却走了软编,CPU负载直接起飞。排查方式是在代码里输出当前到底用了哪个编码器,线上出现问题时要能定位。

内存上涨一般不是泄漏,而是编码器buffer和RTMP发送队列堆积。弱网时librtmp发不出去,数据积压在队列里,内存自然越来越高。给发送队列设置最大长度是有必要的,满了就丢最旧的数据帧,或者切低码率。这也是librestreaming相对原始的地方,它有队列机制,但没有默认的上限配置,需要业务层自己兜底。

6. librestreaming与FFmpeg推流方案怎么选

6.1 核心维度对比

为了帮大家做决策,我把两种方案的差异整理成一张表:

对比维度librestreamingFFmpeg
集成成本低,源码导入即可,集采集编码推流一体高,需要编译so库、自建采集管线
APK体积增加较小,主要增加librtmp和x264大,一个完整FFmpeg静态库动辄几十MB
启动耗时几百毫秒内可完成依赖初始化参数,通常更慢
硬编兼容性内置探测和降级逻辑需要自己写降级逻辑
协议扩展RTMP为主,部分分支支持RTSPRTMP/RTSP/HLS/SRT等全协议
自定义滤镜可以在GL管线里插帧处理可通过libavfilter实现
跨平台Android为主全平台通用

选择其实很清楚:如果你的目标是Android单端的RTMP直播推流,追求快速上线、低包体积、少踩坑,librestreaming是很合适的选择。这也是我个人在实际项目中的体会——用它做移动端直播推流,省掉的踏坑时间远超预期。而如果你要做的平台不止Android,或者后续要接HLS、SRT等其他协议,或者你的推流服务端对接特别复杂,那FFmpeg的完整生态仍然是更稳妥的路。

6.2 混用与迁移的思考

还有个折中思路,不少团队也这么干:采集、预览、硬编用Android原生,封装协议和网络传输用FFmpeg或者librtmp。librestreaming的封装层本来就是librtmp,如果你对这个库不够放心,完全可以把它替换成自己基于FFmpeg的封装,只要接口保持类似的结构。很多项目的最终形态都是这样混着来的。

我自己的当前方案是:在Android端继续用librestreaming作为主力推流方案,因为它的GL管线跟Camera耦合度高,改造成本低,日常运维够用。但我在服务端和转码链路上一直在用FFmpeg做横向扩展,两者各司其职。如果你的业务中Android推流是最短路径,那这个库可以放心用;如果未来有跨平台需求,现在就留好协议抽象层,为以后演进留一条退路。

最后再分享一个小技巧:对librestreaming这类底稿开源的库,不要过度依赖它提供的默认参数,建议把分辨率、码率、GOP、编码器类型、队列上限这些全部透出到运营后台。直播线上出问题的时候,能远程调整参数,比让用户重新发版救火有效得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询