ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Android投屏Demo实战:MediaProjection与MediaCodec编码传输全解

Android投屏Demo实战:MediaProjection与MediaCodec编码传输全解 简介Android端投屏demo是一份面向Android开发者的示例工程其核心是演示基于MediaProjection API的手机屏幕镜像到电视、电脑或投影仪等设备的完整流程。压缩包大小33.55MB共2000个文件以class、png、xml、json、jar、java等为主涵盖编译产物、界面资源、配置清单、源代码及辅助脚本便于直接导入项目分析运行。已有7795人学习下载。项目代码中详细实现了运行时权限申请、MediaProjectionManager获取、ScreenCaptureService搭建、VirtualDisplay与ImageReader捕获屏幕帧、MediaCodec编码及音视频传输等关键环节并附带了录制与回放功能。开发者可据此快速搭建投屏应用理解屏幕捕获、编码和跨设备传输的完整链路同时参考其目录组织与资源管理方式为二次开发或性能优化提供直接样板。 手机投屏这个需求说大不大说小真不小。会议共享、远程协助、游戏观战、多屏协作哪个场景离了“把这块屏幕搬到另一块屏幕上”都不行。我最早接触“Android端投屏demo”时以为就是调个系统API的事真动手才发现屏幕采集、编码、传输、解码整条链路上每一步都有隐藏关卡。这篇文章把我自己从零撸出来的一套可运行Android投屏demo完整拆开讲核心链路怎么设计、关键代码怎么落、黑屏和权限这些坑分别怎么踩过去都会聊到。适合正准备做投屏功能、或者想拿一个最小可用demo做二次开发的Android开发者参考。我采用的技术主路线是系统自带的MediaProjection采集屏幕用MediaCodec硬编码成H.264走自建的Socket通道发给接收端接收端拿到码流后硬解并渲染到Surface。这套方案链路短、可控性强延迟基本能压在100毫秒左右不依赖第三方SDK也不碰厂商私有协议。接下来我按照从方案选型到代码落地、再到排坑调优的顺序把每一步都掰开揉碎讲清楚。1. 项目概述这个投屏demo到底解决了什么问题1.1 典型使用场景与用户痛点投屏demo本身不是一个完整产品它更像是投屏功能的“最小可行性验证”。我见过最典型的几种需求分别是手机画面实时共享到会议室大屏解决开会时一群人围着一块小屏幕看PPT的尴尬工程师远程帮客户排查手机问题需要看到对方屏幕才能定位是哪个界面卡了还有游戏玩家想把操作画面分享给队友观看对延迟特别敏感。这些场景共同的要求是实时性要好、画面不能糊、接入成本要低。很多现成方案要么绑死了特定硬件比如必须用同一品牌电视要么走云端中转导致延迟高到没法用。自己做一套基于局域网传输的投屏demo就能在最基础的层面把这些问题验证清楚后续无论往哪个方向扩展都有底子。1.2 主流投屏方案对比为什么我选自研市面上常见的投屏技术路线有好几条我整理了一张对比表方便你快速判断自己的场景适合哪条路方案传输基础延迟表现优点缺点MiracastWi-Fi Direct100~200ms系统级支持无需装App厂商兼容性参差扩展性差Google CastWi-Fi局域网200ms以上生态成熟支持后台播放定制困难需要认证DLNA/AirPlay局域网200~500ms多媒体场景体验好屏幕镜像支持弱实时性差自研协议本项目Socket/局域网80~150ms完全可控、可深度定制需要自己处理编码和传输细节从表里能看出来自研方案的核心优势不是“技术碾压”而是可控性。你可以自由决定分辨率、码率、是否加密、是否支持双向控制也可以针对特定硬件做优化。我在做这个demo时的目标很明确不需要覆盖所有设备只需要在自己的Android手机和接收端之间建立一条足够稳、足够快的通路。1.3 为什么这套链路值得自己写一遍很多刚入门的朋友问过我“直接用scrcpy改一改不行吗”行但改完你得到的还是scrcpy的思路。自己写一套MediaProjection加MediaCodec的链路你会被迫理解每一帧画面是怎么从屏幕进入编码器、变成二进制流、穿过网络、再被还原成画面的。这些知识点在面试、性能调优、适配问题排查中都能直接复用。2. 核心链路拆解从采集到显示的四段式架构2.1 采集端MediaProjection和VirtualDisplay的原理与局限Android屏幕采集的官方入口是MediaProjection它本质上是系统给你发的一张“截屏许可证”。调用createScreenCaptureIntent()会弹出一个用户确认对话框只有用户点了允许系统才会把屏幕内容授权给你。这个设计是为了防止App在后台偷偷录屏。拿到授权之后需要创建一个VirtualDisplay虚拟显示器。你可以把它理解成系统里凭空多出来的一块“隐形屏幕”系统会把真实屏幕的内容镜像到这个虚拟屏幕上。创建时需要传一个Surface作为输出目标这个Surface可以直接是编码器的输入Surface也可以是一个普通Surface供你读取像素数据。我在demo里选的是前者让画面直接从VirtualDisplay流向编码器中间不经过CPU拷贝效率最高。需要注意的一个坑是MediaProjection的授权是一次性的进程被杀或者用户主动撤销后需要重新发起授权流程。部分厂商系统还会在后台长时间采集时自动中断投影所以采集必须配合前台服务使用。2.2 编码端MediaCodec硬编H.264的关键参数编码是整个链路里最容易出问题、也最影响画质和延迟的环节。我选择H.264而不是H.265核心原因是兼容性。H.264在Android设备上的硬编解码支持率接近100%H.265虽然在同等码率下画质更好但不少老设备的硬解能力跟不上软解又会带来发热和卡顿。编码器的关键参数配置如下val format MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height) format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface) format.setInteger(MediaFormat.KEY_BIT_RATE, 4_000_000) format.setInteger(MediaFormat.KEY_FRAME_RATE, 30) format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1) format.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileHigh) format.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel32) codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)这里耗费我最多时间的是Profile和Level的选择。Profile决定编码复杂度Level决定分辨率、帧率的上限区间。很多教程只写分辨率不写Profile结果某些设备能编不能解。我实测下来Profile High配合Level 4.0在绝大多数设备上都是安全的1080p30fps的规格正好在这个区间内。2.3 传输层TCP、UDP还是WebSocket传输层选型是个经典的权衡问题。TCP可靠但重传会导致延迟抖动UDP快但丢包会直接造成花屏。WebSocket则是在TCP之上的协议封装天然适合跨语言、跨端的对接场景。我在demo里优先选择TCP理由很实在局域网场景下网络质量本身比较好TCP的拥塞控制机制虽然会引入一定延迟但能够保证画面完整性。对于投屏这种交互场景来说“画面完整但偶尔延迟波动”比“低延迟但画面破损”的体验要好得多。传输协议需要自己定义一个轻量帧格式。我的方案很简单每帧数据前加8个字节的头部前4字节存数据长度后4字节存帧类型关键帧或非关键帧。接收端先读头部再按长度读取完整数据块这样能有效避免TCP粘包和半包问题。2.4 解码端MediaCodec硬解与Surface渲染接收端解码相对简单配置一个解码用MediaCodec把网络收到的数据直接喂给它输出Surface绑到SurfaceView或者TextureView上即可。这里有一个容易踩的坑SurfaceView的画面更新是异步的如果View没有正确进入可见状态可能会导致画面卡在最后一帧。TextureView则更容易和动画、缩放等UI操作配合代价是性能略低于SurfaceView。在demo中我默认使用SurfaceView因为它的性能更好且投屏场景一般不需要复杂的UI叠加效果。如果你的接收端还需要绘制触摸指令、显示控制面板之类的叠加层再换成TextureView会更合适。3. 实操落地从零搭建一个可运行的投屏demo3.1 环境准备与工程配置开发环境方面我使用的是Android Studio最新稳定版SDK版本支持到Android 14编译目标设成34。测试设备我准备了两台一台是小米系的手机负责实测高版本Android和厂商定制的适配问题另一台是老旧的Android 9设备用来验证低版本兼容性。工程结构上我建了两个模块app作为采集端也就是被投屏的手机端receiver作为接收端可以是Android电视、盒子也可以是另一台手机。如果你只想在电脑上看画面接收端也可以替换为PC上的VLC或者ffplay但为了完整性我建议先做Android接收端方便统一用Android Studio调试。项目依赖很少不需要引入额外的第三方库。整个demo的核心代码只有三个类ScreenCaptureService负责采集和编码StreamSender负责网络发送StreamReceiver负责网络接收和解码。这种极简结构方便你把注意力全部放在核心链路上。3.2 权限申请和前台服务声明投屏功能必须申请以下权限FOREGROUND_SERVICE前台服务运行必需FOREGROUND_SERVICE_MEDIA_PROJECTIONAndroid 14新增专门用于媒体投影前台服务POST_NOTIFICATIONSAndroid 13以上需要动态申请通知权限否则前台服务通知不显示Manifest里还要声明前台服务的类型service android:name.ScreenCaptureService android:foregroundServiceTypemediaProjection android:exportedfalse /权限申请顺序是先动态申请通知权限再发起MediaProjection授权。很多新手只处理了MediaProjection的弹窗忘了通知权限结果服务在后台跑了一会儿就被系统杀掉。另外从Android 14开始如果前台服务类型没有正确声明为mediaProjection运行时会直接抛出ForegroundServiceStartNotAllowedException这一条需要特别注意。3.3 采集端编码与发送模块实现采集端代码的核心流程如下通过MediaProjectionManager发起屏幕采集授权在onActivityResult里拿到MediaProjection实例创建编码器并获取输入Surface用输入Surface创建VirtualDisplay循环从编码器输出缓冲区取出编码后的数据通过Socket发送给接收端关键代码片段如下// 申请屏幕采集权限 val mediaProjectionManager getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager startActivityForResult(mediaProjectionManager.createScreenCaptureIntent(), REQUEST_CAPTURE) // 在 onActivityResult 中获取 MediaProjection 并启动服务 override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode REQUEST_CAPTURE resultCode RESULT_OK) { val serviceIntent Intent(this, ScreenCaptureService::class.java) serviceIntent.putExtra(resultCode, resultCode) serviceIntent.putExtra(resultData, data) startForegroundService(serviceIntent) } }在ScreenCaptureService里VirtualDisplay和编码器的绑定是核心val inputSurface codec.createInputSurface() virtualDisplay mediaProjection.createVirtualDisplay( ScreenCastDisplay, width, height, dpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, inputSurface, null, null )当VirtualDisplay创建成功后屏幕内容就会持续不断地流向编码器。此时只需在单独的线程里循环读取编码器输出while (isRunning) { val index codec.dequeueOutputBuffer(bufferInfo, 10_000) if (index 0) { val buffer codec.getOutputBuffer(index) // 写入自定义帧头 编码数据到Socket sendFrame(buffer, bufferInfo) codec.releaseOutputBuffer(index, false) } }这几段代码连起来就是一条完整的采集编码链路。注意dequeueOutputBuffer的超时时间我设置为10毫秒这个值在延迟和CPU占用之间比较平衡。设置太短会导致循环空转浪费电量设置太长则可能在帧率波动时引入额外延迟。3.4 接收端解码与显示模块实现接收端相对简单启动一个ServerSocket监听端口等待发送端连接。连接建立后从输入流中按帧头解析数据块然后喂给解码器。解码器配置val format MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height) codec.configure(format, surface, null, 0) // surface来自SurfaceView codec.start()解码循环while (isRunning) { val index codec.dequeueInputBuffer(10_000) if (index 0) { val inputBuffer codec.getInputBuffer(index) val bytesRead socketInputStream.read(inputBuffer!!) if (bytesRead 0) { codec.queueInputBuffer(index, 0, bytesRead, pts, 0) } } val outputIndex codec.dequeueOutputBuffer(bufferInfo, 10_000) if (outputIndex 0) { codec.releaseOutputBuffer(outputIndex, true) } }看到releaseOutputBuffer的第二个参数我传的是true意思是让解码器把画面直接渲染到Surface上。这一步有个隐藏细节如果你传false解码器不会自动渲染画面就一直在缓冲区里积累表现出来就是“解码在跑但屏幕不亮”。很多黑屏问题其实就出在这一个参数上。3.5 Android高版本适配要点Android 10开始强制分区存储后App不再能随意访问外部存储的任意目录。如果你的投屏demo需要读取相册里的视频文件再投出去切记不能直接拼/sdcard/DCIM/xxx.jpg路径去读而是要用MediaStore或SAF。同理如果接收端需要保存截图或录屏文件也要写入应用专属目录或者通过MediaStore写入公共目录。Android 13和14的适配重点放在通知权限和前台服务类型上。我实测在小米HyperOS和原生Android 14上只要前台服务类型漏了mediaProjection服务必然启动失败。Android 14还要求在前台服务启动后的短时间内必须调用startForeground否则会抛异常。最稳妥的做法是在onCreate里就立即调用startForeground而不是等到网络连接或编码器准备好之后再调用。系统对前台服务启动窗口卡得很严这个时机不能赌。4. 实战排坑那些让人抓狂的问题与解法4.1 投屏黑屏排查链路与方法投屏黑屏是出现频率最高的问题scrcpy和qtscrcpy的用户反馈里也大量提到。我总结下来黑屏基本逃不出这几个原因现象可能原因排查方法发送端黑屏自己看不到画面VirtualDisplay与编码器输入Surface绑定失败检查Surface是否有效打印编码器状态接收端黑屏但CPU在跑解码器没有渲染到Surface检查releaseOutputBuffer第二个参数是否为true画面卡在第一帧解码器缺少关键帧确认编码器配置了KEY_I_FRAME_INTERVAL花屏后黑屏网络丢包导致码流损坏降低码率或改用TCP传输我最常遇到的情况是编码器输出正常、网络也正常但接收端一直黑屏。后来定位到是解码器初始化时没有拿到正确的宽高信息。解码器需要知道分辨率才能配置输出缓冲区而我最初写死了1920x1080但发送端实际采集的是手机竖屏分辨率两者不匹配导致解码器罢工。解决方法是发送端把宽高信息写在自定义帧头的扩展字段里接收端动态配置。4.2 高版本Android的权限与URI坑热词里出现的content://com.ss.android.uri.key/external_root/android/data/...这类路径本质上是分区存储背景下App之间共享文件的URI授权问题。你在投屏demo里如果还要处理“投屏前选一个文件先展示”这类交互就会直接碰到这个问题。一个典型报错是FileNotFoundException原因是拿到了URI却没有持久化读取权限。解决办法是在onActivityResult里调用contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)这样才能在后续的Service里持续读取这个URI指向的文件。另外file://开头的路径在Android 7.0以上默认会被FileUriExposedException拦截所有跨App文件共享都应该使用FileProvider生成content://URI。4.3 老机型与低版本系统兼容热词里有“安卓4.4.2支持的远程投屏”这部分我专门验证过。Android 5.0以下没有MediaProjection API无法通过常规方式采集屏幕只能在root设备上读取/dev/graphics/fb0帧缓冲或者依赖ADB的screenrecord命令抓取。4.4.2还有一种折中做法是用MediaRecorder直接录屏到文件再用流媒体服务器转发但延迟会高到无法接受。如果你的产品必须支持Android 5.0以下设备我的建议是放弃自研采集改接Miracast硬件方案。在软件层面硬撑老设备投入产出比太低。这个demo可以从Android 7.0起步老设备数量已经很少强行兼容反而会拖累整体体验。4.4 延迟、帧率和画质的调优思路调优的核心思路是“先保证可用再追求极致”。当链路能跑通出现画面后延迟偏高是正常的需要逐段排查。我实际测试中发现传输层占用的延迟通常在20~40毫秒编码器本身会引入30~50毫秒解码端还有一帧的缓冲延迟。如果整体延迟超过200毫秒优先检查网络发送端是否因为TCP拥塞而阻塞了编码循环。一个有效的优化手段是设置KEY_LOW_LATENCY为1部分设备的编码器会启用低延迟模式。但实测发现这个参数在部分高通芯片上是有效的在联发科上却没什么变化所以不能作为万能药。另一个更通用的方式是降低编码分辨率而不是降低码率把1080p降到720p画面锐度损失不大但编码时间和传输带宽都会显著降低。音频投屏我没把它放进这个demo里原因是音频采集需要额外的AudioRecord权限和AAC编码链路复杂度会翻倍。如果你的业务场景需要声音建议把音频作为一个独立模块后续加上先保证视频链路稳定再扩展音频排错会轻松很多。最后再分享一个我踩了三次的坑不要试图用同一个Socket同时传视频和触摸控制指令。视频数据量大会挤占控制指令的发送时机导致远端操作卡成幻灯片。最佳实践是单独开一条控制通道哪怕多占用一个端口也值得。这个小改动让整个demo的交互体验提升了一个档次你按这个思路去扩展遥控功能时会感谢这个决定。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表