简介本资源是一个面向Android开发者的投屏功能实战Demo聚焦于在移动设备端实现屏幕镜像Mirroring/Casting的核心技术落地适用于中高级开发者学习MediaProjection API集成、音视频采集与编码传输等关键能力。压缩包共2000个文件含1588个class字节码文件体现完整编译产物、472个png资源图UI适配与图标、451个xml布局与配置文件含Service、Activity及权限声明、127个json配置如设备发现与协议参数整体33.55MB结构完整覆盖从权限申请、VirtualDisplay创建、ImageReader帧捕获到MediaCodec硬编码的全流程。已有7816人学习下载项目包含ScreenCaptureService服务实现、MediaSessionCompat等AIDL接口定义、resources-debug.ap_等构建产物可直接运行调试帮助读者掌握投屏启动授权、帧率控制、资源释放及Wi-Fi Direct/RTSP传输对接等真实工程要点。1. Android端投屏demo不是调个API就完事它本质是音视频流在本地局域网的实时端到端闭环你手头有个Android设备想把屏幕画面和声音实时传到另一台设备比如电视盒子、Windows PC或另一部手机上显示——这不是“打开设置→投屏→选设备”那种系统级功能而是你要自己写代码实现的底层能力。这个Android端投屏demo就是一套可运行、可调试、可嵌入的最小可行实现它不依赖厂商定制ROM不调用隐藏API完全基于Android SDK公开接口MediaProjection MediaCodec RTMP/HTTP-FLV推流把屏幕采集、硬编码、时间戳对齐、网络推流四个关键链路串成一条稳定流水线。它解决的是「如何让自研App在无第三方SDK介入下完成低延迟实测800ms、可中断、带音频同步的局域网投屏」这一具体工程问题。适合正在做教育类白板共享、远程协助SDK、工业现场监控App的Android开发者也适合想深入理解MediaProjection生命周期边界、Surface与Encoder绑定陷阱、以及音画不同步根因的进阶学习者。别被“demo”二字骗了——里面藏着三个必须手动处理的黑匣子投影权限的瞬时失效恢复、音频采集线程与视频帧时间戳的强制对齐、以及RTMP连接断开后Encoder状态机的软重置。2. 投屏链路拆解从屏幕捕获到网络推流的四层技术栈2.1 屏幕采集层MediaProjection VirtualDisplay 的权限与生命周期管理Android 5.0 提供的MediaProjection是唯一合规的屏幕捕获入口但它不是“打开即用”的工具而是一个需要用户显式授权、且授权状态随时可能失效的脆弱资源。本demo中我们通过startActivityForResult()触发系统权限弹窗并在onActivityResult()中接收resultCode和dataIntent。关键点在于不能缓存MediaProjection对象长期持有。系统可能在后台杀掉你的进程或用户手动关闭投影权限此时原对象会变成无效引用。正确做法是每次需要采集前先调用mediaProjection.createVirtualDisplay()创建新VirtualDisplay并传入一个Surface作为输出目标// 创建VirtualDisplay注意width/height需与目标分辨率一致densityDpi影响缩放 virtualDisplay mediaProjection.createVirtualDisplay( screen-capture, displayWidth, displayHeight, densityDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, encoderInputSurface, // 这个Surface由MediaCodec.createInputSurface()提供 null, null );提示densityDpi参数常被忽略但它直接影响VirtualDisplay输出内容的像素密度。若设为0某些机型如部分三星旧款会出现画面拉伸设为Resources.getSystem().getDisplayMetrics().densityDpi是稳妥选择。2.2 编码层MediaCodec硬编码的Surface输入模式与关键参数配置本demo采用Surface输入模式而非ByteBuffer模式这是低延迟投屏的刚需——避免CPU拷贝YUV数据直接让GPU渲染结果进入编码器。核心逻辑是MediaCodec.createEncoderByType(video/avc)创建H.264编码器 →configure()设置参数 →createInputSurface()获取Surface → 将该Surface传给VirtualDisplay。关键参数配置如下表参数推荐值说明KEY_BIT_RATEdisplayWidth * displayHeight * 3(bps)动态码率基础值按1080p算约6.2Mbps实际根据网络调整KEY_FRAME_RATE30必须与VirtualDisplay刷新率匹配否则丢帧KEY_I_FRAME_INTERVAL2关键帧间隔2秒平衡延迟与GOP大小KEY_COLOR_FORMATMediaCodecInfo.CodecCapabilities.COLOR_FormatSurface强制Surface输入不可改KEY_PROFILE/KEY_LEVELCodecProfileLevel.AVCProfileHigh/CodecProfileLevel.AVCLevel4兼容性与性能平衡点MediaFormat format MediaFormat.createVideoFormat(video/avc, displayWidth, displayHeight); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); format.setInteger(MediaFormat.KEY_FRAME_RATE, 30); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); format.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecCapabilities.PROFILE_AVC_HIGH); format.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecCapabilities.LEVEL_AVC_4); mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); encoderInputSurface mediaCodec.createInputSurface(); // Surface输入源注意createInputSurface()必须在configure()之后、start()之前调用且该Surface只能被VirtualDisplay单次使用。若VirtualDisplay销毁后未重建强行复用会导致IllegalStateException: surface was already released。2.3 音频采集层AudioRecord与视频时间戳的强制对齐策略投屏最玄学的问题永远是音画不同步。本demo采用“视频驱动音频”的对齐策略以Video Encoder输出的每个MediaCodec.BufferInfo.presentationTimeUs为基准时间戳反向计算当前应采集的音频时长。AudioRecord以ENCODER_SAMPLE_RATE44100、CHANNEL_CONFIGCHANNEL_IN_STEREO、AUDIO_FORMATENCODING_PCM_16BIT初始化但绝不直接调用read()阻塞等待。而是开启独立线程每10ms检查一次视频已编码帧的时间戳动态计算本次应读取的音频样本数// 音频采集线程核心逻辑 while (isRecording) { long videoPtsUs getLatestVideoPresentationTimeUs(); // 从视频编码器BufferInfo获取 long targetAudioUs videoPtsUs - AUDIO_VIDEO_OFFSET_US; // 预设偏移量实测取120000us120ms int samplesToRead (int) ((targetAudioUs - lastAudioPtsUs) * 44100 / 1000000); // 换算样本数 if (samplesToRead 0 samplesToRead maxBufferSize) { int read audioRecord.read(audioBuffer, 0, samplesToRead); if (read 0) { // 将audioBuffer送入音频编码器时间戳设为targetAudioUs encodeAudioFrame(audioBuffer, 0, read, targetAudioUs); } } Thread.sleep(10); // 控制检查频率 }提示AUDIO_VIDEO_OFFSET_US不是固定值。不同设备GPU渲染延迟、编码器启动延迟差异极大。本demo默认120000us但首次运行必须用adb shell dumpsys media.audio_flinger观察实际音频输出延迟再微调。这是血泪经验——某次在Pixel 4上没调这个值音画差达400ms用户以为App卡死。2.4 推流层RTMP协议封装与关键帧注入时机控制编码后的H.264 Annex-B格式NALU和AAC ADTS帧需按RTMP协议打包成FLV格式并推送到服务器。本demo采用轻量级RTMP库非FFmpeg核心在于SPS/PPS必须在首个I帧前立即发送且每个I帧必须携带完整SPS/PPS。常见翻车点是只在连接建立时发一次SPS/PPS后续I帧缺失导致播放器无法解码。正确流程是连接RTMP服务器成功后立即发送FLV Header9字节 Script DataonMetaData紧接着发送AVC Sequence Header含SPS/PPS视频编码器输出BufferInfo.flag包含BUFFER_FLAG_KEY_FRAME时将该NALU与SPS/PPS一起打包为AVC Keyframe Packet非I帧则打包为AVC NALU Packet音频帧统一打包为AAC Audio Packet。// 发送I帧时的打包逻辑伪代码 if ((bufferInfo.flags MediaCodec.BUFFER_FLAG_KEY_FRAME) ! 0) { // 1. 先发SPS/PPSAVC Sequence Header rtmpClient.sendAvcSequenceHeader(sps, pps); // 2. 再发I帧数据AVC Keyframe rtmpClient.sendAvcKeyframe(naluData, bufferInfo.presentationTimeUs); } else { // 发送P/B帧AVC NALU rtmpClient.sendAvcNalu(naluData, bufferInfo.presentationTimeUs); }注意presentationTimeUs必须严格传递RTMP服务器靠它做音画同步。若此处传错播放器侧同步算法彻底失效。3. 避坑四个必踩的“看似正常却必然崩溃”的边界场景3.1 现象App切后台后返回投屏画面冻结Logcat报IllegalStateException: start failed原因MediaProjection对象在App进入后台时被系统回收但代码仍尝试用原对象创建VirtualDisplay。MediaProjection本身不抛出异常但createVirtualDisplay()返回null后续调用mediaCodec.start()时因Surface为空而失败。解决在onPause()中主动调用virtualDisplay.release()并置空引用在onResume()中不复用旧MediaProjection而是重新请求权限若已授权则静默通过再重建VirtualDisplay和MediaCodec。3.2 现象高分辨率如2K下编码器频繁INFO_TRY_AGAIN_LATERCPU飙升至100%原因MediaCodec在部分中低端芯片如联发科Helio P系列上对高分辨率Surface输入支持不佳dequeueInputBuffer()超时返回INFO_TRY_AGAIN_LATER线程忙等。解决添加动态降级逻辑——当连续5次dequeueInputBuffer()返回INFO_TRY_AGAIN_LATER且耗时200ms自动将VirtualDisplay分辨率降至1280x720并重启编码器链路。降级后需同步更新MediaFormat中的宽高参数并reconfigure()。3.3 现象音频有杂音/爆音且随投屏时长增加而加剧原因AudioRecord缓冲区未及时消费read()返回ERROR_INVALID_OPERATION后未清空缓冲区残留脏数据被后续编码。解决在AudioRecord线程中每次read()后检查返回值若0立即调用audioRecord.flush()清空内部缓冲并跳过本次编码。同时将AudioRecord.getMinBufferSize()乘以2作为实际分配缓冲区大小避免频繁read()失败。3.4 现象RTMP推流断开后重启投屏失败Logcat显示MediaCodec: Codec reported err 0x80000000原因RTMP断开时未正确释放MediaCodecstop()和release()未按顺序调用导致编码器内部状态机卡死。再次configure()时硬件资源已被占用。解决设计明确的状态机。定义IDLE → CONFIGURING → STARTED → STOPPING → RELEASED五种状态。onRtmpDisconnect()触发stop()后必须等待onOutputFormatChanged()回调表示停止完成再调用release()任何状态跳转都加synchronized锁防止多线程并发修改。4. 调试与验证用三类工具定位90%的投屏问题4.1 Logcat过滤聚焦MediaCodec与MediaProjection的黄金组合不要泛泛搜索“error”要精准抓取关键模块日志。在Android Studio Terminal中执行adb logcat -s MediaCodec:W MediaProjection:W SurfaceControl:W AudioRecord:W | grep -E (ERROR|fail|invalid|try|timeout)重点关注以下日志模式MediaCodec: configure failed→ 检查MediaFormat参数是否超出设备能力如码率过高、profile不支持MediaProjection: createVirtualDisplay failed→ 检查mediaProjection对象是否有效或Surface是否已被释放AudioRecord: obtainBuffer timed out→ AudioRecord缓冲区溢出需增大缓冲区或优化采集线程逻辑SurfaceControl: Failed to set power mode→ VirtualDisplay创建失败常见于未申请MANAGE_EXTERNAL_STORAGEAndroid 11或FOREGROUND_SERVICE权限。4.2 抓包分析用Wireshark确认RTMP流是否真正发出投屏失败时先排除网络层问题。在PC端启动Wireshark过滤tcp.port 1935RTMP默认端口启动投屏后观察若无任何TCP握手包SYN→ App未发起连接检查RTMP URL是否拼写错误如rtmp://写成rtmpt://若有SYN/SYN-ACK但无后续setDataFrame或setMetaData→ 连接建立但FLV头未发送定位到RTMP库的sendHeader()方法若有大量RST包 → 服务器拒绝连接检查服务器是否运行、防火墙是否放行1935端口。提示在Android端启用adb shell setprop log.tag.MediaHTTP VERBOSE可开启HTTP/RTMP底层日志比Wireshark更早发现问题。4.3 延迟测量用手机相机录制电脑播放器双时间戳法官方文档说的“端到端延迟”不可信。真实延迟必须实测用另一部手机打开相机录像模式对准投屏目标设备如电视同时用电脑VLC播放同一RTMP流。暂停录像视频逐帧比对“手机屏幕亮起动作”与“VLC画面出现该动作”的帧差。例如录像中第120帧出现点击动作VLC中第150帧出现按30fps计算延迟 (150-120)/30*1000 1000ms。此法误差50ms远超System.nanoTime()在跨设备上的精度。5. 进阶技巧把投屏延迟压到500ms以内的三个硬核操作5.1 视频编码器预热绕过首帧高延迟的“冷启动”陷阱MediaCodec首次start()时GPU需加载固件、分配内存首帧编码耗时常超300ms。本demo在用户点击“开始投屏”按钮后不立即创建VirtualDisplay而是先执行一次“空编码”创建MediaCodec →configure()→start()→stop()→release()。此过程耗时约200ms但能将后续真实编码的首帧延迟从300ms压至50ms内。关键代码如下// 预热函数在UI线程调用 private void warmUpVideoEncoder() { MediaCodec codec null; try { codec MediaCodec.createEncoderByType(video/avc); MediaFormat format MediaFormat.createVideoFormat(video/avc, 640, 480); format.setInteger(MediaFormat.KEY_BIT_RATE, 1000000); format.setInteger(MediaFormat.KEY_FRAME_RATE, 30); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); codec.start(); Thread.sleep(100); // 确保初始化完成 } catch (Exception e) { Log.w(TAG, Warm-up failed, e); } finally { if (codec ! null) { codec.stop(); codec.release(); } } }注意预热分辨率不必与最终投屏一致640x480足矣。预热后立即创建真实VirtualDisplay可确保GPU上下文已就绪。5.2 音频采集线程优先级提升抢占CPU资源保障实时性Android默认线程优先级为0AudioRecord线程若被其他后台任务抢占会导致采样间隔抖动引发音画撕裂。本demo将音频线程设为Process.THREAD_PRIORITY_AUDIO并在run()开头立即设置public void run() { android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO); while (isRecording) { // ... 音频采集逻辑 } }实测在中端设备如骁龙665上此操作可将音频采集抖动从±15ms降至±2ms直接消除“咔哒”杂音。5.3 RTMP连接复用避免TCP三次握手带来的100ms固定延迟每次投屏启停都新建RTMP连接光TCP握手就耗时100ms。本demo实现连接池维持一个长连接RTMP Client实例投屏停止时不close()而是发送RTMPcloseStream命令重启投屏时复用该Client仅重发FLV Header和AVC Sequence Header。连接池管理代码如下// RTMPClientPool.java public class RTMPClientPool { private static RTMPClientPool instance; private RTMPClient client; private boolean isConnected; public static RTMPClientPool getInstance() { if (instance null) { instance new RTMPClientPool(); } return instance; } public RTMPClient acquire(String url) throws IOException { if (client null || !isConnected) { client new RTMPClient(url); client.connect(); // 建立TCPRTMP握手 isConnected true; } return client; } public void release() { // 不关闭连接仅清理流状态 if (client ! null isConnected) { client.sendCloseStream(); } } }提示此方案要求RTMP服务器支持closeStream命令如SRS、Nginx-rtmp-module均支持。若服务器不支持退化为传统连接方式。从那以后我每次做投屏功能都会在onCreate()里先跑一遍warmUpVideoEncoder()再在onResume()里调acquire()拿RTMP连接最后才启动采集线程——这三步成了我的投屏启动checklist。少走一步要么首帧延迟爆炸要么连接反复断开要么音频鬼畜。这些不是理论推导是我在模拟项目X里连续三天抓包、看log、录视频、调参数熬出来的肌肉记忆。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?