简介:面向Android直播与多媒体开发的工程实践者,这份资源围绕RTMP协议实现录屏直播推送,完整覆盖屏幕录制、音频采集、底层推流、服务端搭建和流格式校验等核心环节,适合需要把手机画面实时分享出去的场景。压缩包共1409个文件,整体约31.2MB,包含Android工程源码、XML与JSON配置、class与dex构建产物,以及so、jar等底层依赖库;另有Nginx-RTMP服务器搭建文档、FLV分析工具、Gradle配置与构建脚本,目录结构清晰,便于按模块查阅,可辅助环境搭建与二次开发。已有2593人学习,适合需要从零搭建录屏直播应用、深入研究RTMP推流细节,或排查音视频不同步问题的移动开发者。借助工程代码和配套说明,能够理清权限申请、编码参数设置、音频采集、音视频同步、服务器配置及数据校验的实现思路,并参考其中整合与测试案例,减少从零搭建的试错成本。 做Android录屏直播这个需求,我第一次接到时以为只是调个API的事,真正动手才发现链路比想象中长:屏幕画面采集、麦克风录音、H.264/AAC编码、RTMP协议封包、网络推流,每一环都能出幺蛾子。这篇文章就把我用Android实现RTMP录屏直播推流音视频的完整过程拆开讲一讲,包括技术选型、核心参数、可运行的代码骨架,以及我在真机上踩过的坑。如果你正准备做手机直播、远程屏幕分享、设备巡检类的App,这篇文章可以帮你少走不少弯路。
1. 整体思路与技术选型
1.1 录屏直播的核心链路
录屏直播本质上是一条数据流水线:先把屏幕画面和麦克风声音采集到内存,再用编码器压缩成视频流和音频流,最后通过RTMP协议封装并推到服务器。很多新手一上来就翻SDK文档,结果被MediaProjection、VirtualDisplay、MediaCodec、RTMP Client一堆概念绕晕,其实只要抓住这条链路,每一步做什么就清晰了。
画面采集是系统级的,Android提供了MediaProjection API,它会把整个屏幕内容渲染到一个Surface上,我们只需要把这个Surface交给编码器,系统就会把每一帧画面传输进去。音频采集则用AudioRecord拿PCM裸数据,再喂给MediaCodec的AAC编码器。编码之后,视频是H.264裸流,音频是AAC裸流,RTMP推流器负责把这两路数据按时间戳封装成FLV格式,通过TCP长连接发给服务器,服务器再分发给观看端。理解了这条链路,后面所有配置都是在回答一个问题:每一环节怎么拿到正确的数据,并保证它们按同一个时钟往前走。
1.2 为什么RTMP仍然是录屏直播的首选
移动端直播协议很多,HLS、WebRTC、SRT都有人用,但录屏直播这种场景我仍然优先推荐RTMP。原因很简单:RTMP基于TCP长连接,延迟通常在1到3秒,能够满足大部分互动直播的需求;配套生态也非常成熟,Nginx-RTMP、SRS、Red5都可以直接搭建推流服务端,播放端VLC、FFmpeg、各类播放器天然支持。对于开发者来说,RTMP的推流端实现难度也比WebRTC低很多,不用自己处理ICE穿透、DTLS加密、JitterBuffer这些复杂逻辑。
我整理了一个简单对比,方便你根据场景选型:
| 协议 | 延迟 | 复杂度 | 适用场景 |
|---|---|---|---|
| RTMP | 1-3秒 | 低 | 录屏直播、游戏直播、活动转播 |
| HLS | 5-10秒以上 | 低 | 点播、大规模观看,对延迟不敏感 |
| WebRTC | 200-500ms | 高 | 视频会议、连麦互动 |
如果你的业务是“手机屏幕分享给几个人看”,RTMP足够用;如果要做1v1互动会议,再考虑WebRTC。不要一开始就上最复杂的方案,先把链路跑通,再根据需求演进更稳妥。
1.3 推流SDK选型:自研还是用开源
实现RTMP推流有两种路径:自己用librtmp/FFmpeg封装,或者直接用开源项目。我的经验是,除非你有特殊的底层定制需求,否则直接用维护活跃的开源库更划算。目前Android端做得比较顺手的开源库有两个:一个是早期的yasea,它把摄像头推流做得很完善,但作者已经很久不更新,在Android 12以上的权限适配有坑;另一个是pedroSG94的rtmp-rtsp-stream-client-java,它支持摄像头、麦克风、屏幕采集三条采集链路,内部同时封装了RTP和RTMP,文档和示例都比较全,我最后的项目就是基于它改的。
如果你非要从零自研,用librtmp也不是不行,但你要自己处理Surface、MediaCodec和RTMP线程的交互,还要解决时间戳抖动、断线重连、内存复用这些问题,工作量至少多三倍。建议先用成熟SDK把业务跑通,再根据Binder、内存分配这些瓶颈去替换自己写的模块。
2. 环境准备与权限适配
2.1 项目Gradle依赖与权限清单
我用的库是rtmp-rtsp-stream-client-java,它的Maven坐标通常在项目的root build.gradle里添加JitPack仓库,然后在模块里加依赖。写成Gradle配置大致是:
// settings.gradle 或 root build.gradle dependencyResolutionManagement { repositories { maven { url "https://jitpack.io" } } } // app/build.gradle dependencies { implementation 'com.github.pedroSG94.rtmp-rtsp-stream-client-java:rtplibrary:2.2.4' }版本号建议以GitHub最新Tag为准,2.x系列在Android 13实测没问题。AndroidManifest里的权限也不复杂,但每一个都不能少:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />注意ACCESS_NETWORK_STATE最好也加上,用于推流前检测网络状态。如果你的App最低版本低于Android 10,还需要声明WAKE_LOCK防止息屏采集时CPU休眠导致直播中断。
2.2 MediaProjection权限申请细节
MediaProjection是录屏直播的起点,它不是一个普通权限,而是需要用户在系统弹窗里点“允许”的动态授权。申请代码很简单:
MediaProjectionManager manager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE); Intent permissionIntent = manager.createScreenCaptureIntent(); startActivityForResult(permissionIntent, REQUEST_CODE_MEDIA_PROJECTION);核心坑在回调里。onActivityResult返回的resultCode和data必须原样保留,之后创建MediaProjection和VirtualDisplay都要用到。很多同学在这里直接把data序列化或者二次封装,结果拿到一个无效的MediaProjection,Surface画面上黑屏。
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_CODE_MEDIA_PROJECTION) { if (resultCode == RESULT_OK && data != null) { mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data); // 到这里才能开始创建VirtualDisplay和推流 } } }另外,一次授权的MediaProjection在用户撤销权限或系统重启后会失效,需要重新申请。所以直播界面上最好留一个“停止并重新授权”的按钮,遇到权限失效时引导用户回来。
2.3 Android 14前台服务类型限制
我在Android 14(API 34)真机调试时,发现直接启动前台服务会崩,因为系统强制要求声明前台服务类型。如果你的App targetSdk是34或更高,启动录屏/录音的服务必须显式声明类型:
<service android:name=".StreamService" android:foregroundServiceType="mediaProjection|microphone" android:exported="false" />同时,在代码里启动服务时也要把类型传进去:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { startForegroundService(new Intent(this, StreamService.class)); } else { startService(new Intent(this, StreamService.class)); } // 在Service的onStartCommand里 startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION | ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);不传类型在Android 14上会直接抛出ForegroundServiceStartNotAllowedException或SecurityException。这也是我把“环境准备”单独拎出来讲的原因,很多低级崩溃都是权限和配置没对齐,而不是推流逻辑本身有问题。
3. 核心细节解析与实操要点
3.1 屏幕采集尺寸与码率如何定
录屏直播不像摄像头推流那样受光学传感器限制,屏幕分辨率由设备决定,但我们不能直接拿物理分辨率去采集。比如一台2K屏手机,用原生分辨率编码,码率至少要20Mbps以上,推上去不卡才怪。实际项目中需要把画面缩放到一个合适的档位。
| 档位 | 宽高比 | 建议视频码率 |
|---|---|---|
| 480p | 720x480 | 800-1200kbps |
| 720p | 1280x720 | 1.5-2.5Mbps |
| 1080p | 1920x1080 | 3-5Mbps |
码率估算可以套一个粗公式:每像素每帧约0.1-0.3bit,720p就是1280*720=921600像素,乘30帧约2760万像素每秒,按0.07bit折算大约2Mbps。如果画面大多是静态界面,码率可以降到1.2Mbps;如果录游戏,同一分辨率建议再加一档码率,不然运动会糊掉。
创建VirtualDisplay时,要注意传入的width、height和densityDpi。dpi不直接影响编码像素,但它会影响系统按什么分辨率去合成画面,传错会导致画面错位或拉伸。一个保守做法是densityDpi = getResources().getDisplayMetrics().densityDpi,分辨率单独传一个合适的录制尺寸。
3.2 MediaCodec H.264编码器配置的关键参数
用MediaCodec做H.264编码,参数配置直接决定推流画面质量和兼容性。我常用的配置块长这样:
MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); format.setInteger(MediaFormat.KEY_BIT_RATE, 2_000_000); format.setInteger(MediaFormat.KEY_FRAME_RATE, 30); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2); format.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);KEY_COLOR_FORMAT必须设置成COLOR_FormatSurface,因为我们要把VirtualDisplay的输出Surface直接交给编码器,这是最省内存、性能最好的路径,不需要在Java层拷贝像素。KEY_I_FRAME_INTERVAL指关键帧间隔,单位是秒,我习惯设2秒。关键帧间隔越小,播放端拖动和弱网恢复越快,但码率会升高;设太大,切流或断线重连时要等很久才能出画面。
编码器配置时还有一个隐藏参数:Profile。Android的硬编解码器默认可能输出Baseline Profile,很多老设备播放没问题,但某些云服务转码会不支持。如果你要保证兼容性,可以显式指定KEY_PROFILE和KEY_LEVEL,比如AVCProfileHigh配合AVCLevel31,但这需要在适配不同芯片时代上测试,建议先保持默认跑通再调。
3.3 音频采集:AudioRecord与AAC编码注意点
音频部分相对简单,但有两个容易忽略的点:采样率和声道数。Android的AudioRecord支持44.1kHz,这个采样率对应AAC LC编码效率最高。声道建议用单声道,因为录屏直播多数场景是讲解人声或环境音,单声道不仅能省一半音频码率,还能降低编码负载。
int sampleRate = 44100; int channelConfig = AudioFormat.CHANNEL_IN_MONO; int encoding = AudioFormat.ENCODING_PCM_16BIT; int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, encoding); AudioRecord audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate, channelConfig, encoding, bufferSize);采集到PCM数据后,要用MediaCodec的AAC编码器转码。AAC编码器不是按帧RAW输出就能直接推流的,它会输出AudioSpecificConfig(包含采样率索引、声道配置等),这个配置必须放在RTMP的AAC sequence header里,播放器才能正确解码。用rtplibrary这类SDK,它内部会帮你发这套头,但如果你自研推流,千万别漏掉。
另外一个大家容易忽略的问题是音频焦点。如果设备上正在播放音乐,应用直接开始录音,有可能被系统混音。建议在开始直播前申请AudioManager.requestAudioFocus,保证录制过程中系统不会插入通知音或降低音量,否则直播里会飘进各种“叮咚”提示声。
3.4 时间戳与音视频同步
RTMP推流最容易被忽视的就是时间戳,这里踩坑的人非常多。RTMP里每一帧数据都带一个相对时间戳,单位是毫秒。视频和音频必须使用同一条时钟,通常以视频帧率作为基准,音频帧按实际播放时间累加。
我遇到过一个典型问题:画面正常但声音越来越快,最后和画面完全对不上。排查下来发现是音频时间戳用了SystemClock.elapsedRealtime()的绝对值,而不是从推流开始计算的相对值。正确做法是在推流开始时记录一个基准时间startTime,每写入一帧音频/视频时,时间戳都减去这个基准。
在MediaCodec里,编码后的BufferInfo.presentationTimeUs已经是相对系统启动的微秒时间,我们需要把它除以1000转成毫秒,并且判断这一帧是否在基准之后,否则会因为那一两帧的偏差导致视频流被播放器判定为乱序而丢弃。这个问题我在第5章再详细说。
4. 实操过程与核心环节实现
4.1 最小可跑通的主流程代码骨架
我给你一个可运行的最小主流程,用rtplibrary的RtmpCamera类可以同时管理屏幕采集和音频采集。连接服务器后,它会自动把MediaProjection和AudioRecord的流送到编码器并推出去。
// 初始化推流器 RtmpCamera rtmpCamera = new RtmpCamera(callback); rtmpCamera.setOnlyAudio(false); rtmpCamera.setVideoResolution(1280, 720); rtmpCamera.setVideoBitrate(2_000_000); rtmpCamera.setFps(30); rtmpCamera.setAudioBitrate(96_000); rtmpCamera.setSampleRate(44_100); rtmpCamera.setAudioChannels(1); rtmpCamera.prepareAudio(); // 创建MediaProjection后,把屏幕采集绑定到推流器 MediaProjection mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data); rtmpCamera.start(mediaProjection); // 推流 rtmpCamera.connect("rtmp://192.168.1.100:1935/live/stream"); rtmpCamera.startStream();注意这里的顺序:先准备音频和视频编码器,再连接RTMP,连接成功后再启动推流。如果你把connect放到prepareAudio之前,可能在连接建立时音频编码器还没准备好,导致服务器收到流后只有视频没有声音。
如果你不想依赖库,而是直接用MediaCodec+VirtualDisplay自己封装,主流程也差不多:创建编码器Surface,把它传给VirtualDisplay,编码器输出喂给RTMP客户端。区别只是要自己维护两个线程:编码回调线程和RTMP发送线程,以及一个缓冲队列。
4.2 推流地址与参数调优参考
RTMP推流地址格式一般是:
rtmp://<服务器IP或域名>:<端口>/<应用名>/<流名称>比如我本地用Nginx-RTMP测试,配置的application叫live,流名我习惯用设备编号stream_01,完整地址就是rtmp://192.168.1.100:1935/live/stream_01。服务器不一定限制推流地址格式,但流名尽量不要用中文和特殊字符,某些播放器解析不了。
参数调优没有一个万能答案,但可以按这个初始值起步:
| 参数 | 建议值 | 说明 |
|---|---|---|
| 分辨率 | 1280x720 | 兼顾清晰度和上传带宽 |
| 视频码率 | 2Mbps | 720p30帧的动态画面下限 |
| 帧率 | 30fps | 超过30场景收益有限,耗电翻倍 |
| I帧间隔 | 2秒 | 弱网恢复快 |
| 音频码率 | 96kbps | 中文人声足够清晰 |
| 音频采样率 | 44100 | 兼容性最好 |
| 声道 | 1 | 省码率、省功耗 |
4.3 本地验证:Nginx-RTMP + FFplay
推流代码写完后,第一件事不是直接上生产服务器,而是在本地把链路验证通。最简单的方式是用Nginx-RTMP模块搭一个本地流媒体服务。我通常用Docker起服务,省去编译模块的麻烦:
docker run -d -p 1935:1935 -p 8080:80 \ --name rtmp-server \ -e RTMP_APP=live \ alfg/nginx-rtmp播放端用FFplay拉流:
ffplay -fflags nobuffer -analyzeduration 1000000 rtmp://127.0.0.1:1935/live/stream_01这里要注意,Android真机和电脑必须在同一局域网内,才能用电脑的IP访问。如果Android模拟器,还要把地址换成10.0.2.2才能访问宿主机。这一步验证完,再考虑公网服务器,否则你连到底是网络问题、编码问题还是RTMP握手问题都分不清。
5. 常见问题与排查技巧实录
5.1 真机会遇到的5个高频问题
我把录屏直播开发中常见的问题整理成了一张表,每个都是我或朋友团队实际遇到过的:
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 推流后播放端黑屏 | 编码Surface未正确传给VirtualDisplay | 检查VirtualDisplay的Surface和MediaCodec的输入Surface是否同一个对象 |
| 有画面无声音 | 音频焦点被系统静音;AAC sequence header没发 | 申请音频焦点;查看AAC配置头是否正确 |
| 画面卡顿但CPU不高 | 视频码率设置过高,网络带宽不够 | 降低码率或分辨率 |
| 延迟越来越大 | 没有用相对时间戳,音视频时间基准不一致 | 重写时间戳逻辑,统一减去推流起始时间 |
| 推流10分钟后自动断开 | 手机系统限制了后台服务 | 使用前台服务并申请忽略电池优化 |
5.2 黑屏问题排查顺序
黑屏是录屏直播最常见的“第一款”问题。按我的经验,按这个顺序排查基本能定位:
第一,确认MediaProjection授权是否有效。如果没有调用getMediaProjection,或者resultCode不对,VirtualDisplay根本创建不了,日志里通常会有MediaProjection相关报错。
第二,确认VirtualDisplay的Surface来自编码器而不是普通SurfaceView。VirtualDisplay的第五个参数surface必须是视频编码器的输入Surface,如果你自己创建了一个SurfaceView的Surface传进去,那只是显示在本地,并不会进编码器。可以用mediaCodec.createInputSurface()拿到编码器输入Surface。
第三,检查I帧。播放器要等到关键帧才能开始解码,如果编码器配置里I_FRAME_INTERVAL太大,比如20秒,你播放器打开后可能前十几秒都是黑屏。我习惯设2秒,既不影响码率,又能让新加入的播放器快速出画面。
按完这三步,90%的黑屏问题都能解决。
5.3 音视频不同步与延迟优化
音视频不同步的根源往往是音频时间戳和视频时间戳不是同一条基准线。MediaCodec输出的视频presentationTimeUs可以直接除以1000作为毫秒时间戳,但音频的PCM是连续数据流,需要自己算好每一帧的播放时长。每帧AAC在44.1kHz单声道下大约是21.33ms,所以每一帧时间戳可以直接累加这个值。如果你发现音频越来越快,大概率是在用采集到的系统时间而不是“上一个时间戳+帧时长”。
延迟优化方面,有几个见效快的操作:把RTMP推流的chunkSize设大一点,比如4096或8192,可以减少小包数量降低CPU占用;播放端使用低延迟模式,VLC的network-caching调到300ms以内;服务端关闭GOP缓存或设置小一点的play_buffer_size。如果延迟到了3秒以上还不能接受,就要考虑从推流和播放两端一起压,而不是只在推流端努力。
5.4 厂商限制与前台服务
国产手机厂商的后台限制是录屏直播最大的“隐形杀手”。小米、华为、OPPO、vivo都会默认禁止应用在后台使用麦克风或屏幕采集,如果用户把直播切到后台,推流会在一分钟内被截断。我的方案是必须创建前台服务,同时请求用户把应用加入“后台运行不受限制”的白名单。
另外,屏幕录制时如果手机息屏,某些系统会暂停MediaProjection画面输出,导致直播画面定格。我在项目里做了两个兜底:推送服务持有WAKE_LOCK保持亮屏(或者提示用户开启“直播时保持亮屏”);同时监听ACTION_SCREEN_OFF,如果息屏就自动暂停推流,避免把静止画面或隐私界面传给观众。
adb调试时可以用下面这行命令查看当前系统是否允许后台录屏,定位权限被系统回收的问题:
adb shell dumpsys media.projection如果输出里看不到你的包名,说明MediaProjection已经被系统回收了,需要重新走一遍授权流程。
写在最后
个人体会:录屏直播这个功能,最难的不是某个API不会调用,而是整个链路太长,出了问题不知道怪谁。我建议你第一次开发时一定先按“最小闭环”跑通:本地Nginx服务器 + 固定分辨率 + 固定码率 + 一个按钮开始/停止,不要一上来就做美颜、字幕、弹幕这些周边功能。链路越短,定位问题越快。
另外,务必准备一台中低端Android真机做测试,很多编码器配置在旗舰机上没问题,换到低端机上直接黑屏或掉帧。等低端机能稳定运行了,再逐步放开分辨率、码率这些参数。最后再分享一个小技巧:在推流页面上加一个“实时码率”显示TextView,用RtmpCamera回调里的onRtmpStats更新,真机调试时非常有帮助,能一眼看出是编码器跟不上还是网络带宽不够,比logcat直观得多。
本文还有配套的精品资源,点击获取