
简介面向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这些复杂逻辑。我整理了一个简单对比方便你根据场景选型协议延迟复杂度适用场景RTMP1-3秒低录屏直播、游戏直播、活动转播HLS5-10秒以上低点播、大规模观看对延迟不敏感WebRTC200-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:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.RECORD_AUDIO / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION / uses-permission android:nameandroid.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序列化或者二次封装结果拿到一个无效的MediaProjectionSurface画面上黑屏。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 14API 34真机调试时发现直接启动前台服务会崩因为系统强制要求声明前台服务类型。如果你的App targetSdk是34或更高启动录屏/录音的服务必须显式声明类型service android:name.StreamService android:foregroundServiceTypemediaProjection|microphone android:exportedfalse /同时在代码里启动服务时也要把类型传进去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以上推上去不卡才怪。实际项目中需要把画面缩放到一个合适的档位。档位宽高比建议视频码率480p720x480800-1200kbps720p1280x7201.5-2.5Mbps1080p1920x10803-5Mbps码率估算可以套一个粗公式每像素每帧约0.1-0.3bit720p就是1280*720921600像素乘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之前可能在连接建立时音频编码器还没准备好导致服务器收到流后只有视频没有声音。如果你不想依赖库而是直接用MediaCodecVirtualDisplay自己封装主流程也差不多创建编码器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兼顾清晰度和上传带宽视频码率2Mbps720p30帧的动态画面下限帧率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_APPlive \ 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直观得多。本文还有配套的精品资源点击获取