Android屏幕共享与远程控制:MediaProjection到事件注入的全链路实践

Android屏幕共享与远程控制:MediaProjection到事件注入的全链路实践 做Android投屏、录屏、远程协助、设备监控这几类需求MediaProjection API基本是绕不开的一环。这个API是Android官方给第三方应用开放的屏幕采集入口用户授权一次之后系统把屏幕画面交给你的App后续不管是硬编码推流、边录边传还是配合输入事件注入做远程控制全看你怎么拆解。这篇文章是这个系列的第二篇上一篇讲了MediaProjection的基础授权流程和录屏保存这一篇往里走一层目标是搭一条相对完整的“屏幕共享 远程控制”链路。我会把整个方案拆成两条线来讲上行链路负责采集屏幕画面、编码、推流下行链路负责接收远端控制指令、做坐标映射、注入触摸和按键事件。两条线之间通过信令通道连接这样手机屏幕上的画面能实时出现在远端远端的操作也能落回这台设备形成一个闭环。文章涉及工程配置、授权生命周期、VirtualDisplay与MediaCodec的搭配、事件注入的权限边界、坐标换算和常见问题的排查思路适合正在做投屏、远程协助、企业设备管控类App的Android开发者参考。1. 屏幕共享方案的整体拆解1.1 为什么选用MediaProjection先回答一个基础问题Android里做屏幕捕获可选的路径其实有好几条但绝大多数场景下MediaProjection是最合理的选择。root方案能直接读SurfaceFlinger的截屏接口功能强、权限大但root本身就把用户门槛抬高了一大截普通商业App不可能要求用户root手机。无障碍服务可以拿“截屏”能力例如AccessibilityService配合takeScreenshot API但Android 11之前这个接口还不能稳定地在所有设备上工作而且它适合“抓一帧”不适合持续采集高帧率视频流。系统定制接口属于厂商ROM私有能力只适合少部分设备不具备通用性。MediaProjection是Android 5.0开始引入的官方API它的核心价值在于第三方应用不需要系统签名不需要root只要用户明确授权就能拿到屏幕内容的实时Surface。用官方的话说它是“用户信任的代理”——用户点了确认你才有资格看到屏幕上发生了什么。这套机制保证了安全边界也给了开发者一个相对干净的入口。当然它也有明显的限制授权是一次性的授权结果跟Activity生命周期绑定系统同时只允许一个MediaProjection实例存在从Android 14开始使用MediaProjection必须搭配特定类型的前台服务。这些约束在开发中都会遇到文章后面逐一展开。1.2 双链路架构上行采集、下行控制整个远程控制方案我习惯把它拆成两条互不干扰又互相依赖的链路。上行采集链路是数据通道。MediaProjection创建一个VirtualDisplayVirtualDisplay充当一个虚拟屏幕把真实屏幕内容镜像到一块Surface上。这块Surface可以交给MediaCodec的输入侧由硬件编码器实时编码出H.264/HEVC流然后通过网络协议推给远端。这条链路的重点参数是分辨率、帧率、码率、关键帧间隔以及如何做动态码率控制保证弱网环境下的流畅度。下行控制链路是信令通道。远端设备电脑、平板、另一台手机发来触摸坐标、点击动作、按键事件Android端收到这些消息后先做坐标系换算再把它们还原成Android可识别的MotionEvent和KeyEvent最后通过系统级接口注入到事件队列。这条链路的核心问题是延迟控制、坐标精度和事件注入的权限实现。两条链路不是完全独立的远端看到的是上行链路采集的画面用户基于这块画面做出操作操作又通过下行链路回到本机。所以“端到端延迟”不单单是编码推流的延迟而是采集延迟、网络传输延迟、事件注入与画面反馈延迟的总和。做这类方案时心里先有这张全景图后面调优才知道瓶颈卡在哪一环。1.3 技术选型对比与取舍Android端完成屏幕捕获与编码后网络传输层通常有三个选择。一是自建私有协议用WebSocket或TCP传输H.264 Annex-B裸流。实现最简单适合快速出Demo或者设备端和播放端都是自己可控的场景。缺点是数据包丢失恢复难、播放器兼容要自己适配生产环境一般只作为兜底方案。二是RTSP/RTP。传统流媒体协议配套的VLC、FFmpeg生态成熟但是想在Android端自己打RTP包再控制拥塞窗口工作量不算小还得处理NAT穿越问题。三是WebRTC。底层用了SRTP、NACK重传、JitterBuffer、拥塞控制把这些东西全包好了音视频领域公认的低延迟实时传输方案。缺点是集成复杂度高有信令服务器和ICE/STUN/TURN的维护成本而且Android原生的WebRTC库需要自己编译或引入预编译包工程体量一下子上来。我的建议是如果你做的是产品原型或者公司内部的远程协助小工具先用WebSocket推Annex-B流快速把链路跑通如果目标是上线商用、面对复杂的家用网络环境老老实实上WebRTC。不要在一开始就陷入底层协议的泥潭后面有的是调优的坑等着你。2. 环境准备与授权链路2.1 工程配置清单开发环境建议用Android Studio最新稳定版Gradle配好JDK 17compileSdk和targetSdk建议直接上到34及以上因为Android 14对MediaProjection的行为有明确的强制约束targetSdk太低反而容易踩到兼容性边缘。AndroidManifest.xml里需要声明这些内容uses-permission android:nameandroid.permission.FOREGROUND_SERVICE / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION / uses-permission android:nameandroid.permission.POST_NOTIFICATIONS / uses-permission android:nameandroid.permission.SYSTEM_ALERT_WINDOW / service android:name.ScreenCaptureService android:foregroundServiceTypemediaProjection android:exportedfalse /逐条解释一下FOREGROUND_SERVICE是启动任何前台服务的基础权限所有targetSdk 31的应用都要求动态判断是否已授予。FOREGROUND_SERVICE_MEDIA_PROJECTION是Android 14新增的类型权限如果你targetSdk是34且前台服务的type包含mediaProjection就必须在清单里写明否则运行时直接SecurityException。POST_NOTIFICATIONS是Android 13引入的通知栏权限前台服务需要给用户展示常驻通知你没有这权限通知发不出来服务进程也可能被系统回收。这个可以直接在运行时申请用户拒绝的话前台服务也还是能起来但尽量引导用户打开。SYSTEM_ALERT_WINDOW用于远程控制的悬浮控制按钮、操作提示浮层不属于MediaProjection必需但做这类交互几乎都会用到。需要用户单独去“悬浮窗”设置页授权无法用普通运行时弹窗搞定。另外如果你要把录屏文件保存到外部存储再分享给其他App还会涉及FileProvider的授权问题这块放到后面踩坑部分细说。2.2 授权流程的正确写法MediaProjection授权流程并不复杂但写法上有几个容易被忽略的细节。先看核心代码class MainActivity : AppCompatActivity() { private lateinit var mediaProjectionManager: MediaProjectionManager private val projectionRequest registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result - if (result.resultCode RESULT_OK result.data ! null) { val serviceIntent Intent(this, ScreenCaptureService::class.java) serviceIntent.putExtra(resultCode, result.resultCode) serviceIntent.putExtra(resultData, result.data) startForegroundService(serviceIntent) } else { // 用户在授权弹窗点了拒绝或者在Android 14上系统收回了授权 Toast.makeText(this, 未获得屏幕捕获授权, Toast.LENGTH_SHORT).show() } } fun startCapture() { mediaProjectionManager getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager projectionRequest.launch(mediaProjectionManager.createScreenCaptureIntent()) } }在ScreenCaptureService里拿到resultCode和data之后才去创建MediaProjection实例class ScreenCaptureService : Service() { private var mediaProjection: MediaProjection? null override fun onCreate() { super.onCreate() startForeground(NOTIFICATION_ID, createNotification()) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val resultCode intent?.getIntExtra(resultCode, 0) ?: 0 val resultData if (Build.VERSION.SDK_INT 33) { intent?.getParcelableExtra(resultData, Intent::class.java) } else { Suppress(DEPRECATION) intent?.getParcelableExtra(resultData) } if (resultCode ! RESULT_OK || resultData null) { stopSelf() return START_NOT_STICKY } val projectionManager getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager mediaProjection projectionManager.getMediaProjection(resultCode, resultData) mediaProjection?.registerCallback(object : MediaProjection.Callback() { override fun onStop() { // 系统通知授权已收回这里要释放编码器和VirtualDisplay releaseResources() stopSelf() } }, Handler(Looper.getMainLooper())) startScreenCapture(mediaProjection) return START_NOT_STICKY } }这段代码里有几个关键点全部是实践中的教训不要在Activity的onActivityResult里直接调用getMediaProjection。正确的做法是把授权结果交给前台服务由服务创建MediaProjection否则Activity重建或退出后MediaProjection实例很容易拿到却因为缺少有效的生命周期管理变得不稳定。授权结果是一次性的。resultCode和resultData传递到服务之后再重启服务不能靠服务自带粘性重启恢复——用户授权已经不复存在必须重新走一遍授权弹窗流程否则getMediaProjection会抛出SecurityException。要注册MediaProjection.Callback。系统资源紧张或用户主动在系统设置里关闭投屏权限时系统会通过onStop回调通知你你需要在这里做资源释放。忽略了它可能出现授权收回后编码器还占着不放的问题最终导致一连串异常。Android 14上有一种典型情况用户点击授权弹窗时弹窗被系统回收或者授权过程中Activity被其他App打断resultCode会被置为0resultData为null。代码里如果没做空判断直接getMediaProjection崩溃率直接拉满。这段代码能帮你把这类情况兜住。2.3 Android 14前台服务与单会话限制Android 14API 34对MediaProjection的使用提出了非常明确的强制要求网上很多资料说得比较模糊我结合踩坑经验讲清楚。第一targetSdk 34及以上的AppMediaProjection只能在startForegroundService之后使用。也就是说你必须先启动一个前台服务再创建VirtualDisplay。而启动前台服务之前又必须已经完成了MediaProjection的用户授权。顺序不能反用户没授权你不能启动带mediaProjection类型的前台服务前台服务没起来你不能创建VirtualDisplay。第二前台服务类型里必须显式声明android:foregroundServiceTypemediaProjection并且运行时startForeground要传FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION。如果你用旧代码直接startForeground(id, notification)不带类型也会被系统拦下来。第三同一时间系统全局只允许一个MediaProjection实例。如果应用A正在投屏应用B再去申请MediaProjection要么拿不到授权要么采集不到画面。这个限制对用户侧行为也有感知你投着屏一旦通知栏里出现另一款录屏App的采集提示你的画面会被系统直接中断。所以代码里遇到Failure类型的异常优先检查是不是有另一个采集会话占着。前几年我们做远程协助功能时第一批线上崩溃全是因为服务类型没适配Android 14用户点击“开始协助”后服务一直拉起失败。如果你还在低版本SDK上开发务必把这些约束提前落实。3. 屏幕采集链路从VirtualDisplay到硬编码3.1 VirtualDisplay参数与屏幕适配授权拿到MediaProjection之后核心工作就是创建VirtualDisplayval metrics resources.displayMetrics val displayWidth metrics.widthPixels val displayHeight metrics.heightPixels val displayDensity metrics.densityDpi virtualDisplay mediaProjection?.createVirtualDisplay( ScreenShareVirtualDisplay, displayWidth, displayHeight, displayDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, codecSurface, null, Handler(Looper.getMainLooper()) )几个参数逐个说。name是调试标识在dumpsys SurfaceFlinger里能看到最好起一个能定位问题的名字。width/height是虚拟屏分辨率。直接用屏幕物理分辨率能保证画面最清晰但代价是编码压力大、码率需求高。我的建议是共享场景用720p起步目标分辨率根据网络上行带宽动态调整比如上行带宽低时降到640x360。不要迷信1080p很多远端设备看起来也分不清1080p和720p的区别但码率和卡顿的差别是实打实的。densityDpi影响虚拟屏幕的尺寸换算单位。如果设置得很大UI元素在虚拟屏上看起来偏小编码出来的画面文字却很清晰设置很小画面内容偏大但整体锯齿感重。通常直接用系统densityDpi即可不用特殊处理。flags设置VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR时VirtualDisplay会镜像主屏幕如果服务协议里标记了“仅捕获某个窗口”就需要结合MediaProjection的regionCapture能力但普通屏幕共享用AUTO_MIRROR足够。设备横竖屏切换时VirtualDisplay的宽高不会自动跟着变。你需要监听配置变化或屏幕方向在回调里停掉旧VirtualDisplay用新分辨率重建。很多黑屏问题都出现在这里竖屏授权后旋转到横屏VirtualDisplay还按旧的宽高输出播放端就花屏或者只显示一个角落。3.2 Surface直通编码还是ImageReader逐帧采样VirtualDisplay需要一个Surface作为输出终点。这里有两个选择性能差异巨大。第一是MediaCodec输入Surface直通。做法是给MediaCodec设置createInputSurface()得到一块inputSurface然后直接把这块Surface丢给VirtualDisplay。整个链路是VirtualDisplay - GPU合成 - 编码器输入Surface - H.264流中间没有应用层拷贝延迟最低CPU占用最省。屏幕共享场景首选这种方案没有之一。代码示意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, bitRate) format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate) format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval) format.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR) codec MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC) codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) inputSurface codec.createInputSurface() codec.start()第二种是ImageReader逐帧读取。ImageReader会从系统拿到一帧帧的Image数据应用层可以做滤镜、加Logo、OCR识别、截图存盘等额外处理。但代价也很明显每一帧都要从Surface拷贝到应用内存CPU和内存带宽开销大增高分辨率下掉帧是常态。远程控制类的低延迟场景不要用ImageReader做主要采集链路它更适合作为旁路抓帧工具需要额外处理时再用消费线程从ImageReader里takeLatestImage。一句话总结屏幕共享主链路用Surface直通编码ImageReader作为辅助工具处理特殊帧需求别把它当主力。3.3 MediaCodec参数配置与码控策略编码器参数里影响屏幕共享体验的主要是这几类编码格式H.264综合兼容性最好所有播放器都支持H.265HEVC码率更低画质更优但老设备播放器和部分低端机的硬编支持不好。做C端产品优先H.264做特定设备间的私有协议可以考虑H.265。Profile等级屏幕共享不是4K蓝光Main profile即可不要用High profile强行拉复杂度低端设备解码容易出问题。码率模式CBR恒定码率更稳画面内容复杂时波动小适合实时传输通道VBR画质好但码率波动大容易把网络打满。屏幕共享建议CBR或“CBR with limited VBR”。帧率大多数场景30fps足够PPT投影类内容甚至15fps肉眼也察觉不到太大差异。无脑60fps只会在弱网上放大卡顿感。关键帧间隔H.264推流端需要周期性发I帧一般设为1秒或2秒也就是KEY_I_FRAME_INTERVAL1。弱网丢包时远端用关键帧请求重新同步画面间隔太大会导致黑屏时间过长。码率1080p屏幕共享建议3~5Mbps起步720p建议1.5~3Mbps。具体取决于内容复杂度但远程协助类场景往往同时跑着上行视频流和下行控制指令码率不是越高越好。我们线上一般默认2Mbps弱网自动降到800kbps画面有轻微模糊但操作不卡。动态码率控制建议做成一个闭环播放端定期把接收码率、丢包率、RTT通过信令回传给采集端采集端根据这些反馈调整编码器bitrate。MediaCodec在API 19之后支持Bundle方式动态设置KEY_BIT_RATEAPI 21之后还支持KEY_BITRATE_MODE切换。用起来也不复杂。3.4 推流通道的几个可行方案编码器输出的是裸的H.264 Annex-B流。要把这段字节流送到远端根据工程要求选不同方案。方案A私有协议 WebSocket信令。采集端把编码器输出的数据用固定帧结构封装帧头带长度、时间戳、关键帧标记走WebSocket或者TCP发到自己的中继服务播放端收到后再解码。这套方案实现最快做Demo、做公司内部小工具完全够用前面链路跑通后你可以把精力放到协议设计和性能优化上。方案BWebRTC。Android端把MediaCodec的编码流与WebRTC的PeerConnection对接起来不过实际工程中很少有人直接裸手对接更多是采用WebRTC的MediaStream与VideoTrack再配合屏幕采集Surface填充视频帧。这条路能获得一整套拥塞控制、丢包重传、低延迟播放能力代价是集成复杂度高、包体积增大。商用远程协助产品基本都选WebRTC。方案CRTSP/RTP。很多做安防或自研播放器的团队习惯用RTSP但是Android端要自己把H.264打进RTP包还要跑NAT穿透工作量不小。如果对端播放器已经固定用VLC这类支持RTSP的软件可以试试否则不如WebRTC。实操中我用得最顺的组合是开发阶段用“WebSocket H.264裸流 VLC拉流/自研解码器”快速验证采集端各环节是否正常稳定之后再把传输层切成WebRTC。这样每一步都有清晰的可验证节点不会一上来就陷入WebRTC的编译和环境地狱里。4. 远程控制链路坐标映射与事件注入4.1 能做到什么程度先摸清权限边界屏幕共享只解决了“看见”远程控制要解决的是“操作”。这一部分涉及Android输入系统的权限边界必须讲透。Android的输入事件注入核心接口是InputManager.injectInputEvent。但这个接口需要系统签名权限android.permission.INJECT_EVENTS普通第三方App声明了这个权限也拿不到因为应用签名不是系统签名时系统会直接把这个权限忽略掉。所以第三方App在标准Android设备上是没法直接注入一个MotionEvent然后让SystemUI以假乱真的。那日常的“远程控制/远程协助”App是怎么做的主要有三条路第一条通过AccessibilityService。辅助功能服务可以调用dispatchGesture来模拟点击、滑动、长按等手势不需要系统签名但需要用户手动在系统设置里开启无障碍权限。它的问题在于只能模拟手势层面的事件没法精确到坐标位置的底层MotionEvent注入而且一部分输入法、录屏、支付类页面会对辅助功能事件做额外校验手势行为可能不完全等效。优点是兼容性好绝大多数Android手机都支持。第二条在系统定制设备上做。企业级设备、设备管理场景下App可以申请成为DeviceOwner/ProfileOwner配合框架层API实现事件注入这需要设备本身支持或者出厂做定制普通消费者设备做不到。第三条在Android系统内部用shell权限辅助注入。比如adb shell input命令可以注入原生的MotionEvent和KeyEvent它在开发调试阶段特别方便可以直接验证坐标映射和数据链路是否正确。但它同样不能作为普通App的默认能力。所以做C端产品时远程控制方案优先走AccessibilityService dispatchGesture。如果你做的是企业内部设备管控再考虑系统级方案。直接把InputManager的注入代码贴进项目里大概率在真机上拿不到任何效果还容易误导新手。4.2 坐标映射与旋转处理远端的屏幕坐标系和被控端的物理坐标系通常不是1:1对齐的尤其是被控端发生了旋转、分辨率变化、系统导航栏隐藏等情况。坐标映射本质上是一个二维坐标系变换问题需要把远端触点的像素坐标转换成被控端合法的事件坐标。简单场景下远端看到的画面就是被控端当前屏幕画面坐标为远端控件上的相对位置0~1的归一化坐标最稳妥。收到坐标后被控端再换算成物理坐标。如果被控端旋转了VirtualDisplay的分辨率也变了最简单的处理是远端画面同步更新分辨率同时坐标计算按新分辨率重新换算。这里有一个重要细节Android的display旋转会改变WindowManager的defaultDisplay方向而VirtualDisplay创建时设置的分辨率如果没跟着变远端画面会看起来被拉伸。所以服务端收到onConfigurationChanged后要重建VirtualDisplay并重新协商分辨率。坐标发送时建议发送归一化坐标x/width, y/height接收端乘以当前分辨率得到真实坐标。这样不管分辨率怎么变远端坐标都有效。精度控制在float类型足以满足触摸事件的要求不要用int否则低分辨率下坐标误差会被放大。举个例子远端按钮在画面上显示为(1000, 600)实际被控端分辨率是720p远端显示器是1080p正常换算应该先归一化再乘目标分辨率。如果直接在1000/1920 * 1280这样算没问题但一旦中间包含旋转信息最好统一走矩阵变换不然就会出斜着点、点不准的问题。4.3 事件还原从消息到MotionEvent远程控制的消息协议里事件类型需要覆盖ACTION_DOWN、ACTION_UP、ACTION_MOVE、ACTION_CANCEL以及BACK/HOME/菜单键。用AccessibilityService的dispatchGesture模拟点击时天然是手势序列所以不需要自己构造MotionEvent如果用系统级注入则要自己构造MotionEvent并设置eventTime、downTime等时间戳字段。一个建议的消息结构可以参考{ type: TOUCH, action: DOWN, x: 0.521, y: 0.883 }按键事件类似{ type: KEY, keyCode: BACK }如果做系统级注入消息里还需要带时间戳、来源设备、displayId等信息。不过无论走哪条注入路径消息协议尽量把动作类型和归一化坐标作为标准字段后续接不同注入方式时核心逻辑不用大改。这里要提醒一个跟触摸事件直接相关的细节远程操作时用户手指在远端点击要产生“按下去有反馈、抬起来才触发”的效果事件必须成对发送。只发DOWN不发UP屏幕上就像被一只手按住不放应用层会一直收到MOVE事件体验非常差。我们的做法是在协议层强制校验每个DOWN在超时时间内必须配对UP或CANCEL否则重发或作废。4.4 端到端延迟优化远程控制体验的黄金标准是“所见即所得、所点即所得”。延迟高了用户会明显觉得“这个远程软件卡”哪怕帧率很高也不舒服。采集端的优化重点在减少缓冲区。MediaCodec Surface直通已经是低延迟采集的正解给编码器设置较低的“videoBitrate”本身不影响延迟但KEY_LOW_LATENCY如果设备支持能显著降低编码缓冲值得在配置里尝试打开。部分高通和联发科芯片的编码器支持这个参数实测能降低20~50ms的端到端延迟。传输端优化重点在拥塞控制和丢包处理。局域网下延迟通常几十毫秒跨公网后要处理抖动和丢包WebRTC的NACK重传机制能保证显示画面不花屏代价是增加一点延迟。微信小程序、Web端播放H.264裸流时尽量选择延迟在1秒内的播放内核不然操作反馈会非常难受。控制端的优化重点是反馈与误差修正。远程点击之后被控端执行动作画面变化又通过上行链路传回来这个来回就是一个完整RTT。如果整个链路RTT是200ms用户会明显感觉“点下去愣一下”。优化方案包括在保证画面质量的前提下适当降低分辨率以减小码率把控制指令的优先级设计得高于视频帧避免大帧堆积远端收到画面变化后结合生产者的时间戳做播放延迟对齐。5. 实战踩坑与问题排查5.1 常见问题速查表现象可能原因排查方向授权弹窗点了“允许”但收不到OKAndroid 14授权结果被系统回收Activity被重建检查resultCode与resultDataActivity onSaveInstanceState保存requestCode授权流程不要跨进程视频画面全黑/花屏VirtualDisplay分辨率与Surface不匹配编码器输入格式不支持dumpsys SurfaceFlinger查看VirtualDisplay状态检查ColorFormat是否COLOR_FormatSurface换H.264 MainProfile测试声音缺失没有采集MIC或系统内录Android内录方案本身复杂确认需要录音权限系统内录需要AudioPlaybackCapture API单独实现编码器启动崩溃Device上缺少对应MIME硬件编解码器并发编码占用使用MediaCodecList检测设备编码能力降级软编方案确认没有其他投屏会话占着资源远端播放卡顿、花屏码率过高或网络拥塞关键帧间隔太长开启动态码率KEY_I_FRAME_INTERVAL设为1播放端支持关键帧请求辅助功能手势失效部分页面拦截辅助事件手势序列不完整检查是否用了dispatchGesture且回调正常需要支持“抬起”时再出发授权收回但服务还在跑未注册MediaProjection.Callback必须注册onStop回调在回调里释放所有资源并停止前台服务后台运行被杀前台服务类型不完整国产ROM白名单限制确认FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION引导用户加白、锁后台5.2 我印象最深的三个坑第一个坑是授权与服务启动的顺序。我们第一版远程协助功能用户点按钮后直接startForegroundService结果Android 14上抛了ForegroundServiceStartNotAllowedException原因是授权还没完成就启动服务被系统拦截。查了一下午最后发现官网文档写明必须“先授权 - 再启动对应mediaProjection类型的前台服务”。顺序颠倒权限白搭。第二个坑是录屏文件分享时的FileProvider授权问题。有段时间我们做“录屏后分享”功能文件写在外部存储目录然后用FileProvider分享给微信结果对方打开就报“无法访问该文件”。排查下来是URI授权时机的问题用FileProvider.getUriForFile之后commit权限的Intent里必须带上FLAG_GRANT_READ_URI_PERMISSION而且这个Intent要在使用位置构造不能复用旧Intent。配合录屏场景还要小心外部存储目录在不同Android版本下的路径差异比如Android 10之后data目录不能直接读取必须用MediaStore或App专属目录。如果你们也遇到过类似content://fileprovider路径下文件打不开的bug优先检查FLAG_GRANT_READ_URI_PERMISSION是不是丢了一半。第三个坑是双端画面旋转导致的花屏。我们早期在平板上远程协助时竖屏授权开始后用户旋到横屏远端画面就变成纵向拉伸的错位图。后来定位到VirtualDisplay没有重新创建分辨率还是旧的。处理方式是监听Configuration变化然后销毁VirtualDisplay重新创建同时把新的分辨率通过信令通知远端播放器重新协商。这之后即使在PPT演示、视频播放等频繁旋转的场景下画面也能保持正常。5.3 调试工具与日志定位思路远程控制类问题的定位光靠界面表象猜是非常低效的。我建议从底层到上层逐步排查。首先是Surfaces层状态。用adb shell dumpsys SurfaceFlinger --list能看到所有Surface的名字和尺寸VirtualDisplay的Surface应该在里面。如果尺寸对不上问题基本出在VirtualDisplay参数或分辨率协商上。然后是编码器状态。用adb shell dumpsys media.codec可以查看当前编码器实例的配置、码率、帧率确认是不是有多个编码器并发或者编码参数与预期不符。再往上就是网络层。在采集端和播放端同时打印日志记录发送序列号、接收序列号、丢包数、RTT。这些数据对判断“卡顿是编码导致还是网络导致”特别有用。我们内部把日志关键字统一成“SRC_SEND”和“DST_RECV”这种前缀通过logcat filter能快速筛出两个端点的时序。辅助功能的问题可以通过AccessibilityService的onGesture和dispatchGesture回调确认事件是否被系统接受。很多情况下辅助功能没被用户开启是主要问题优先提示用户开启。写在后面投屏和远程控制这套方案技术点不算复杂真正复杂的是把授权生命周期、采集链路、网络传输、事件注入这些环节串在一起。我最初做远程协助模块时一直想把屏幕采集做到极致后来发现真正的复杂度在生命周期管理和稳定性上授权结果被系统回收、Android 14服务类型不匹配、VirtualDisplay随着旋转频繁重建、码率在弱网上突然飙升。这些问题中的每一个单拎出来都不难解决但组合在一起时没有经验的人往往会被绕得晕头转向。最后分享一个我个人的习惯做这类跨端联调项目千万别只在模拟器上验证模拟器对MediaProjection的支持和真机差别很大尤其是编码器行为和前台服务限制。备一台Android 14的真机把授权、采集、编码、推流、注入全链路跑通一遍比看十篇文档都管用。这套方案的后端扩展空间也很大比如把采集端换成PC端手机端做成控制端就变成了另一套“手机控制电脑”的产品核心架构完全能复用。做完Android端之后有空我再单开一篇讲讲PC端的配合方案。