资讯动态

Android录屏直播实战:从MediaProjection采集到RTMP推流全链路解析

发布时间:2026/10/13 13:05:38 来源:尧图企业网站定制
简介面向 Android 开发者的录屏直播技术资源涵盖屏幕采集、音频录制、RTMP 推流与流媒体服务端配置解决从手机画面声音抓取到实时推流上行的完整链路搭建。压缩包共 1409 个文件、约 31.2MB其中 java/class/dex 为源码与编译中间产物xml/json 提供布局与配置参考jar/so 集成依赖及 RTMP 对接库pdf 文档讲解 Nginx RTMP 服务器搭建7z 内为 FLV 流分析工具还附有可直接安装验证的 apk。包内代码与实践文档配套涵盖 MediaProjection 权限申请与录屏实现、AudioRecord 音频同步采集、librtmp 推流调用、Nginx RTMP 模块配置和 FLV 封装校验等要点有助于开发者理解录屏直播全流程作为二次开发或排错对照。已有 2603 位用户学习使用适合需要快速落地 Android 录屏推流功能的中高级开发者。1. 为什么录屏直播要选 RTMP先分清“录文件”和“推流”两件事同样一部手机把屏幕录成 MP4 和把屏幕实时推到直播服务器难度不在一个量级。很多人第一次做 Android 录屏直播上来就用 MediaRecorder 录屏录完才发现拿到的是文件不是流等文件边传边播画面已经落后十几秒。真正要落地的链路是屏幕像素一路经过 VirtualDisplay、H.264/AAC 编码、FLV 封装最后由 RTMP 分块推到服务器观众端延迟控制在几秒以内。RTMP 之所以在这个场景里绕不开是因为直播平台和 CDN 的推流入口几乎都兼容它天然适合把 Android 端的录屏信号送出去。这篇文章就按“链路 → 采集编码 → 推流 → 避坑”的顺序把这条路讲透。2. 先把链路拆清楚RTMP 推送音视频的架构与协议选择2.1 一条完整数据流屏幕 → Surface → 编码器 → FLV → RTMP录屏直播和本地录制的最大区别是数据不能等写盘之后再处理。实时链路里每一步都在跟时间赛跑只要某一环出现排队延迟就会立刻体现在播放端。常见的 Android 录屏直播链路是这样走的用户授权屏幕捕获拿到MediaProjection。MediaProjection.createVirtualDisplay()把屏幕画面投到一个Surface上。这个Surface同时是MediaCodec视频编码器的输入。编码器把 RGBA 或 YUV 数据编码成 H.264 码流输出到 ByteBuffer。音频侧用AudioRecord采集麦克风 PCM 数据喂给另一个MediaCodec编码成 AAC 码流。视频 H.264 和音频 AAC 分别按 FLV Tag 封装Tag 头部记录类型、时间戳、数据长度。封装好的 FLV 数据通过 RTMP 分块消息发送到直播服务器服务器再转给播放端。这条链路里最容易被忽略的是第 5 步。很多人把MediaCodec编码出来的数据直接往 socket 里写播放端自然认不出来。RTMP 推流并不是“裸传编码包”它要求数据先包成 FLV 的格式再切成 RTMP chunk。理解了这个后面看代码就不会乱。2.2 RTMP 分块机制时间戳、消息流与实时性RTMP 基于 TCP默认端口 1935。它本身不是一种编码格式而是一种“容器 传输协议”。握手完成后所有数据都是按 chunk 传输的。默认 chunk size 是 128 字节较大的消息会被拆成多个 chunk接收端再拼回完整消息。每条 RTMP 消息由 Basic Header、Message Header 和 Extended Timestamp 组成。Basic Header 里携带 chunk stream idMessage Header 里携带消息类型、消息长度、时间戳和 stream id。消息类型里我们常用的是0x08音频消息0x09视频消息0x12AMF0 数据消息0x13AMF0 命令消息0x14Set Chunk Size时间戳是播放端做音画同步的主要依据。Android 的MediaCodec输出的presentationTimeUs是微秒递给 RTMP 时要除以 1000 转成毫秒。如果这个时间戳不从同一块单调时钟来就会出现画面越走越慢或者声音对不上的问题。RTMP 的另一个特点是“推流命令先行”。连接建立后必须依次完成connect、createStream、publish这几个命令服务器才会开始接收媒体数据。很多自定义实现翻车都是因为 publish 命令还没完成就开始发视频帧服务器直接断连。2.3 什么时候该选 RTMP协议对比与适用边界做 Android 推流很多人会纠结要不要直接用 WebRTC。这里给一个务实的选型对比协议延迟服务端兼容性Android 侧工作适合场景RTMP2~5 秒几乎所有直播平台/CDN 都支持成熟库多系统 API 配合即可手机录屏直播、泛直播场景WebRTC200ms~1 秒需要自建或购买 WebRTC 网关推流端和播放端都要做信令/ICE 处理互动连麦、低延迟问答HLS5~15 秒CDN 支持好但延迟高一般用于播放端不适合实时推流点播、直播回放SRT1~3 秒支持在内部传输链路公网 CDN 支持参差底层是 UDP需要处理丢包弱网专线、非公网直播如果是给某个直播平台做大屏共享、游戏录屏、在线教学这类场景RTMP 是最省事的方案。它不是延迟最低的但是“推流端工作量、服务器兼容、播放端覆盖”三者里最平衡的。这里我一般会建议先用 RTMP 把整条链路跑通验证录屏采集和编码没问题如果未来对延迟有更高要求推流端协议不变交给服务端把 RTMP 转成 WebRTC 给播放端而不是一开始就在客户端全上 WebRTC。3. Android 录屏采集与编码MediaProjection MediaCodec 组合实战3.1 获取屏幕捕获权限最小代码与回传处理Android 录屏的第一步是向用户申请屏幕捕获授权。这个授权不是普通运行时权限而是系统弹窗让用户确认“是否开始录制或投射”所以必须走onActivityResult回传。// 申请屏幕捕获权限 private static final int CAPTURE_PERMISSION_REQUEST_CODE 1001; private MediaProjectionManager projectionManager; private void startScreenCapture() { projectionManager (MediaProjectionManager) getSystemService( Context.MEDIA_PROJECTION_SERVICE); Intent captureIntent projectionManager.createScreenCaptureIntent(); startActivityForResult(captureIntent, CAPTURE_PERMISSION_REQUEST_CODE); } Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode CAPTURE_PERMISSION_REQUEST_CODE) { if (resultCode RESULT_OK data ! null) { MediaProjection projection projectionManager.getMediaProjection(resultCode, data); if (projection ! null) { startEncodingAndPushing(projection); } } else { // 用户拒绝授权这里结束流程并提示 } } }这段代码里有两个关键点。第一createScreenCaptureIntent()返回的 Intent 必须用startActivityForResult来启动不能直接startActivity。第二从onActivityResult拿到的resultCode和data必须原封不动传给getMediaProjection()否则拿到的投影对象可能是空或者无效的。还有一个隐藏点授权弹窗只在本次启动流程中有效。一旦进程被杀下次录屏必须重新申请授权不能把它当成永久开启的状态。这也是录屏直播必须配合前台服务的原因之一。3.2 创建 VirtualDisplay 并绑定编码器输入拿到MediaProjection之后下一步是创建视频编码器并把编码器的输入 Surface 交给 VirtualDisplay。这里推荐直接使用createInputSurface()让 MediaCodec 从 Surface 上直接读取画面避免显式处理 YUV 数据。// 视频编码器参数H.264 MediaFormat videoFormat MediaFormat.createVideoFormat(video/avc, 1280, 720); videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, 1_500_000); videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 24); videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2); videoFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 0); MediaCodec videoEncoder MediaCodec.createEncoderByType(video/avc); videoEncoder.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); Surface inputSurface videoEncoder.createInputSurface(); videoEncoder.start(); // 把屏幕画面直接投到编码器输入 Surface VirtualDisplay virtualDisplay mediaProjection.createVirtualDisplay( ScreenLiveDisplay, 1280, 720, 320, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, inputSurface, null, null);参数这里值得多说几句。分辨率我建议先跑 1280x720不要一上来就 2K。屏幕共享场景里静态文字和高对比度内容多码率给 1.5Mbps 起步画面复杂时再往上调。帧率 24fps 足够录屏不像游戏那么吃帧。KEY_I_FRAME_INTERVAL是两个关键帧之间的秒数设成 2 秒方便播放端快速起播和切换。KEY_MAX_INPUT_SIZE设为 0意思是不限制单个输入缓冲区大小让编码器自己决定。Surface 输入模式下这个值通常不会被用到但设 0 能避免一些机型对多余内存的校验。3.3 用 AudioRecord 采集麦克风并编码 AAC音频部分最直接的做法是用AudioRecord采集麦克风 PCM再用另一个MediaCodec编码成 AAC。如果只需要录外部声音麦克风是通用性最好的选择系统内录音需要额外处理先不展开。// 音频采集参数 int sampleRate 44100; int channelConfig AudioFormat.CHANNEL_IN_MONO; int encoding AudioFormat.ENCODING_PCM_16BIT; int minBuffer AudioRecord.getMinBufferSize(sampleRate, channelConfig, encoding); int audioBufferSize Math.max(minBuffer, sampleRate * 2); AudioRecord audioRecord new AudioRecord( MediaRecorder.AudioSource.MIC, sampleRate, channelConfig, encoding, audioBufferSize); // AAC 编码器 MediaFormat audioFormat MediaFormat.createAudioFormat( MediaFormat.MIMETYPE_AUDIO_AAC, sampleRate, 1); audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, 64000); audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, audioBufferSize); MediaCodec audioEncoder MediaCodec.createEncoderByType(audio/mp4a-latm); audioEncoder.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); audioEncoder.start(); audioRecord.startRecording();采集线程的读法要单独注意。不能每次把 PCM 直接塞进编码器必须先dequeueInputBuffer拿到可用缓冲区再把数据拷贝进去。byte[] pcm new byte[audioBufferSize]; long audioStartNs System.nanoTime(); while (isPushingRunning) { int readLen audioRecord.read(pcm, 0, pcm.length); if (readLen 0) { continue; } int inputIndex audioEncoder.dequeueInputBuffer(10_000); if (inputIndex 0) { ByteBuffer inputBuffer audioEncoder.getInputBuffer(inputIndex); inputBuffer.clear(); inputBuffer.put(pcm, 0, readLen); long ptsUs (System.nanoTime() - audioStartNs) / 1000; audioEncoder.queueInputBuffer(inputIndex, 0, readLen, ptsUs, 0); } }这段代码里时间戳用的是System.nanoTime()相对启动时刻的差值保证音频时间轴是单调节奏。如果直接用System.currentTimeMillis()一旦系统时间被修改推流就会出问题。另外dequeueInputBuffer返回-1时说明编码器暂时忙不过来这时直接丢弃这一帧音频数据不要原地等待否则采集线程会卡住整个推流流程。4. RTMP 推流上屏两种落地路径与关键参数4.1 面向上线的路径先找成熟推流库把主链路跑通自己完整实现 RTMP 协议不是不可以但要处理握手、AMF0 命令、chunk 拆分、ACK 回包、弱网重连工作量远超想象。所以我一般建议第一版先用成熟的推流库或云厂商 SDK 把链路跑通再根据实际需要决定要不要自研。这里不贴某个库的具体坐标因为版本更新太快你应按照项目当前依赖管理工具选择最新的集成方式。调用示意大概是这样的LivePusher pusher new LivePusher(rtmp://your-host/live/streamKey); pusher.setVideoEncoder(videoEncoder); pusher.setAudioEncoder(audioEncoder); pusher.setMediaProjection(mediaProjection); pusher.start();实际库的 API 可能不一样但核心输入无非是这三样视频编码器或输入 Surface、音频编码器或 AudioRecord、RTMP 地址。先把这三个对象准备好再用库接上去主链路基本上就能通。我会建议把这个阶段记录下来录屏分辨率、码率、帧率、音频采样率、延迟表现。后面换自研实现时这些记录就是基准线。没有基准线就调优纯属玄学。4.2 自研最小 RTMP 客户端握手、连接与发布流程如果你确实需要自研至少要理解 RTMP 的握手和命令顺序。下面是一个最小骨架用于建立 TCP 连接并完成握手不包含生产级异常处理。public class RtmpClient { private Socket socket; private DataOutputStream out; private DataInputStream in; public void start(String host, int port) throws IOException { socket new Socket(host, port); socket.setTcpNoDelay(true); out new DataOutputStream(socket.getOutputStream()); in new DataInputStream(socket.getInputStream()); // C0版本号固定 0x03 out.writeByte(0x03); // C11536 字节前 4 字节时间戳其余填充 byte[] c1 new byte[1536]; new Random().nextBytes(c1); out.write(c1); out.flush(); // 读取 S0 S1再读取 S2 in.readFully(new byte[1537]); in.readFully(new byte[1536]); // C2回显这里简化为全零 byte[] c2 new byte[1536]; out.write(c2); out.flush(); // 握手完成后按顺序发 connect、createStream、publish 命令 // 这部分涉及 AMF0 编码建议直接复用成熟实现 } }一个常见误区是认为读完服务器回包就万事大吉。实际上RTMP 真正的门在命令阶段。连接建立后客户端要发送 connect 命令等服务器回_result再发送 createStream等服务器回 stream id最后才能发送 publish 命令。顺序不对服务器不会报错但媒体数据发过去会被静默丢弃。自研的优势是可控劣势是每个字节都要自己负责。如果只是给一个内部工具用可以考虑如果是面向公众用户的直播功能别低估弱网下的重连和拥塞处理难度。4.3 音视频 Tag 的发送节奏关键帧、时间戳与 FLV 封装RTMP 消息体里装的是 FLV Tag。一个 FLV Tag 由三部分组成Tag Header、Tag Data。视频和音频的 Tag Data 前几个字节还有各自的子头部不能直接塞编码器裸数据。发送视频 Tag 时Tag Header 可以这样构造// 写 FLV 的 3 字节长度字段 private void writeU24(DataOutputStream out, int value) throws IOException { out.writeByte((value 16) 0xFF); out.writeByte((value 8) 0xFF); out.writeByte(value 0xFF); } private void writeFlvTagHeader(DataOutputStream out, int type, int dataSize, int timestampMs) throws IOException { out.writeByte(type); // 9音频8视频 writeU24(out, dataSize); writeU24(out, timestampMs 0xFFFFFF); out.writeByte((timestampMs 24) 0x7F); // TimestampExtended writeU24(out, 0); // StreamID 固定 0 }视频 Tag Data 的头部是 5 个字节第 1 字节高 4 位是帧类型低 4 位是编码 ID0x17表示关键帧 AVC接着 1 字节AVCPacketType0 表示序列头1 表示 NALU最后 3 字节是 CompositionTime没有 B 帧时填 0。音频 Tag Data 的头部是 2 个字节第 1 字节的高 4 位表示音频格式AAC 是 10接着是采样率、采样位数、声道。第 2 字节表示 AACPacketType0 是序列头1 是原始 AAC 数据。封装层最需要盯住的是“先发序列头再发媒体数据”。视频编码器刚启动时会输出包含 SPS/PPS 的csd-0这就是 H.264 的 AVCDecoderConfigurationRecord。音频编码器会输出 AudioSpecificConfig。这两份配置必须分别在第一帧视频/音频数据之前发出去。漏了序列头播放端往往黑屏或者无声但推流端看着一切正常排查起来特别容易漏。发送节奏上视频和音频不能挤在同一个线程里。视频编码器输出一帧数据可能很大TCP 写操作会阻塞如果音频排在后面声音就会顿挫。我一般分成两个发送线程音频线程固定节奏发送视频线程遇到关键帧或网络拥塞时至少保证音频先走。这样就算画面卡顿声音也是连续的观众的体验会好很多。5. 录屏直播避坑黑屏、无声、断连与延迟飙升的 4 个现场5.1 录屏黑屏或绿屏先查 VirtualDisplay 和编码器分辨率现象在部分手机上开始推流播放端能看到的第一帧是黑的或者屏幕画面只有上半部分偶尔出现绿色色块。原因最常见的是 VirtualDisplay 的宽高和编码器 Surface 的宽高不一致导致缩放/裁剪异常。另一个原因是编码器不支持当前颜色格式虽然COLOR_FormatSurface兼容性很好但部分低端设备仍可能失败。解决先固定使用 1280x720确保createVirtualDisplay传的宽高和MediaFormat里完全一致。其次通过MediaCodecList查询当前设备是否支持video/avc和COLOR_FormatSurface不支持的机型直接降级到 854x480。最后检查 VirtualDisplay 的 densityDpi通常传 320 或系统密度即可不要传特别偏的值。5.2 推流后没有声音AAC 序列头和声道参数不一致现象画面正常播放端音量拉满也听不到声音或者只有一个声道有声音。原因音频 AAC 的 AudioSpecificConfig 没有发送或者发送了配置但声道数和音频 Tag 头里的 SoundType 写得不一致。这个问题比视频黑屏还隐蔽因为 RTMP 服务器不会报错播放端也不一定打日志。解决每次MediaCodec输出格式变化时把csd-0保存下来在音频原始数据发出之前先发一个 AACPacketType0 的序列头。同时AudioRecord 如果按单声道采集FLV 音频 Tag 头部的声道位要写 0按双声道采集才写 1。不同声道配置会直接影响 AudioSpecificConfig 的 channelConfiguration 字段两边必须对齐。5.3 时间戳乱跳导致播放器延迟越来越大现象刚推流时播放正常十分钟后画面延迟从 2 秒涨到 8 秒声音也开始对不上。原因时间戳不是单调递增的。常见情况是音频用了System.currentTimeMillis()视频用了System.nanoTime()两个时钟走时不一致播放端持续做缓冲补偿延迟就被拉大了。解决视频和音频都从同一个单调时钟取时间差。视频侧优先使用MediaCodec.BufferInfo.presentationTimeUs如果它本身是单调的就不必额外处理音频侧用System.nanoTime()相对于启动时刻的差值。推流过程中如果遇到系统休眠导致时间戳跳变可以做一个“时间戳修正”发现当前 pts 比上一帧小或超过阈值时强制把 pts 继续上一帧的节奏递增不要跳到未来。5.4 RTMP 连接频繁断开Chunk Size、ACK 与弱网现象推流开始后 1~3 分钟连接断开日志里偶尔出现“connection reset”或“write timed out”而且大多发生在网络抖动时。原因一是 socket 默认启用了 Nagle 算法小包被缓存后推流延迟升高网络抖动时更容易触发超时二是没有处理和服务器之间的 ACK 消息大消息发送时服务器等不到回包就会断。解决创建 socket 后立刻设置socket.setTcpNoDelay(true)把小的音频包和命令包尽快发出去。其次在握手完成后可以主动发一个 Set Chunk Size 消息把 chunk size 调大减少拆包数量。如果使用的是成熟库这些逻辑通常已经封装好自研时一定要在客户端循环里读取服务器发来的控制消息尤其是 ACK 和 Set Chunk Size不能只发不收。5.5 锁屏或切后台推流被系统回收现象按电源键锁屏后VirtualDisplay 立刻停止画面变成最后一帧过一会儿整个进程被系统杀掉。原因录屏本身需要系统前台服务Android 10 之后对后台启动MediaProjection的限制更严格。没有前台服务进程一旦失去焦点就容易被回收。解决推流开始前启动一个前台服务并在AndroidManifest.xml里声明foregroundServiceType为mediaProjection或camera具体按目标版本要求来。另外在服务里申请PARTIAL_WAKE_LOCK避免息屏后 CPU 休眠影响编码线程。这不是可选项而是录屏直播上线前必须处理的稳定性问题。6. 上线前把延迟和画质调到平衡三个立竿见影的推送技巧第一个技巧是关闭 B 帧。屏幕共享场景里B 帧虽然能省一点码率但会引入解码顺序和显示顺序的差值播放端需要额外缓冲。Android 的 H.264 编码器不一定给你直接的 B 帧开关但可以通过设置 Profile 为 Baseline或者选择低延迟模式来规避。没有 B 帧时CompositionTime 固定为 0FLV 封装和播放器处理都会简单很多。第二个技巧是控制关键帧间隔。很多直播链路会把KEY_I_FRAME_INTERVAL设成 5 秒或更长觉得省码率。但录屏画面里静态内容占比高播放端遇到网络抖动时需要等下一个关键帧才能恢复画面。我一般把 I 帧间隔设成 1~2 秒牺牲少量码率换更快的出图和更稳的弱网恢复。第三个技巧是音频优先。把音频编码线程的优先级调高发送队列里音频 Tag 永远排在视频前面。画面可以因为网络拥堵暂时卡顿但声音一旦断续观众的感官是直接“直播事故”。这是我踩过坑之后留下的习惯音频是直播体验的底线。上线前的调试参数我也给一组常用基线场景分辨率码率帧率GOP通用录屏1280x7201.5~2.5Mbps24fps2s高画质文档/PPT1920x10803~4Mbps30fps2s弱网直播854x480800kbps~1Mbps20fps1s我现在的习惯是先把第一组参数跑通再用真机在弱网下压测观察延迟曲线。低于 1Mbps 的码率要谨慎屏幕内容里一旦出现快速滚动画质会明显崩坏。录屏直播这条路踩坑很多但只要把链路拆开、参数记录下来大部分问题都能用同一套方法查出来。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑