一、背景:同一套会议,三端能力并不对等
项目里会议能力基于 LiveKit。H5 / PC 端用 navigator.mediaDevices.getDisplayMedia() 就能发起屏幕共享;但在 App 内嵌 WebView(尤其 Android) 上,这条路基本走不通。
H5 会议室里对「能不能共享」有明确判断:

原因可以概括成三点:
- API 能力:手机浏览器 / WebView 普遍不支持
getDisplayMedia,即便有也多是桌面 Chrome 那一套。 - 系统权限模型:Android 录屏走的是 MediaProjection,必须弹系统授权页,拿
Intent结果才能采屏;WebView 拿不到这条系统链路。 - 进程与生命周期:投屏在 Android 10+ 要求
mediaProjection类型前台服务 常驻通知;纯 H5 很难稳定满足,切后台就容易被杀或采集中断。
因此产品策略是:
| 端 | 会议室实现 | 屏幕共享 |
|---|---|---|
| H5 / PC | LiveKit JS | getDisplayMedia |
| iOS App | web-view 加载 hybrid 页 |
基本不支持发起 |
| Android App | UTS 原生 MeetingActivity |
MediaProjection + LiveKit Android SDK |
二、整体架构:JS 壳 + UTS 桥 + Kotlin 原生会议室
不是「整 App 重写成原生」,而是 只把会议室(含投屏)下沉到 Android 原生,业务壳仍在 UniApp。

关键模块:
uni_modules/yn-livekit-meeting:UTS 插件(依赖livekit-android:2.28.0)MeetingActivity.kt:会议室 UI、进房、订阅、投屏ScreenShareService.kt:mediaProjection前台服务common/livekit/meeting-native.js:Vue 侧桥接subpages/meeting/room.vue:Android 透明壳,负责凭证与业务 API
进房时壳页拿到 LiveKit url/token 后调用:

三、为什么必须走 Android 原生:MediaProjection 机制
Android 从 API 21 起用 MediaProjection 做「用户授权后的屏幕采集」:
- 通过
MediaProjectionManager.createScreenCaptureIntent()拉起系统授权界面 - 用户同意后,
ActivityResult带回RESULT_OK+data Intent - 用这份
Intent创建MediaProjection,再从 VirtualDisplay 采帧 - LiveKit Android SDK 封装了后续编码 / 发布,业务侧只要把授权结果塞进
ScreenCaptureParams
对应代码在 toggleShare():

这就是「必须原生」的核心:系统授权页只能由 Activity 发起,授权结果也只能由原生拿回。WebView 里的 JS 过不了这道关。
四、实现拆解:从点击「共享屏幕」到对端看到画面
1. Manifest:权限 + 前台服务类型

Android 14 对前台服务类型校验更严,mediaProjection 声明缺失会直接启动失败。
2. 授权回调:先起前台服务,再打开 LiveKit 屏幕轨 
顺序很重要:
- 用户授权成功
- 立刻
ScreenShareService.start()(前台通知) - 再
setScreenShareEnabled(true, ScreenCaptureParams(...)) - 失败则停服务、回滚 UI
授权弹窗期间可能有人已经开了共享,所以回来后还要再判一次互斥。
3. 把系统授权结果交给 LiveKit

这里业务代码不自己管 VirtualDisplay / 编码,只做两件事:
- 把系统
Intent交给 SDK - 在
onStop里停掉自己的前台服务
LiveKit 会发布 Track.Source.SCREEN_SHARE,对端(H5/PC/原生)按屏幕轨订阅即可。
4. 前台服务:为什么「多写一个 Service」

要点:
- Android 10+ 使用 MediaProjection 时,需要符合策略的前台服务,否则系统会中断采集
- 通知文案「正在共享屏幕,点按返回会议」,并
PendingIntent回MeetingActivity startForegroundService/STOP_FOREGROUND_REMOVE成对出现,停止共享、离开会议、异常失败都要ScreenShareService.stop()
五、业务细节:不只是「能采屏」
1. 全员互斥:同时只允许一路屏幕共享
发起前、授权回来后都检查 shareFromId;远端已有人共享时,本端误开会主动停掉:

H5 侧同样有「已有人在共享屏幕」逻辑,两端产品规则一致。
2. 识别屏幕轨:兼容 source / name

这样和 JS 侧判断对齐,避免不同端/SDK 版本字段差异导致「共享了但大屏不切」。
3. 渲染坑:GONE 时不要 init TextureView
共享大屏用 TextureViewRenderer。注释里写得很实在:
shareRenderer只 init 一次;GONE 时 init 会导致 Surface 黑屏要先把容器设为
VISIBLE,再post { bindShareRenderer }
这是 WebRTC/Android 渲染里的常见坑:Surface 还没就绪就绑轨,画面会一直黑。
4. 停共享后恢复摄像头
stopShareView 会停服务、清状态;若本端之前开着摄像头,再 restoreLocalCamera(),避免「停共享后摄像头再也开不回来」。
5. 状态同步回 UniApp 壳
原生通过 MeetingBridge.emit("media", { screen: true/false }) 入队,room.vue 短轮询 pollMeetingNativeEvent(),再调业务侧 apiMeetingMedia,让成员列表 / IM 状态和真实媒体轨一致。
六、完整时序

七、工程侧注意点
-
必须自定义调试基座 / 云打包
标准基座不含 LiveKit 原生依赖;
config.json里声明了io.livekit:livekit-android:2.28.0等 Maven 依赖,要由 HBuilderX 编进基座。 -
UTS 分层
Vue →
meeting-native.js→index.uts→MeetingLauncher(Java)→MeetingActivity(Kotlin)。Java 入口是为了避免 UTS 直接啃 Kotlin Companion / 类型转换翻车。 -
双端体验策略
Android 用原生补齐投屏;iOS 仍走 web-view,共享提示不支持------先保证主端能力,而不是强行一套 Web 通吃。
八、总结
Android App 里做会议屏幕共享,本质不是「LiveKit API 换一个写法」,而是:
系统能力(MediaProjection + 前台服务)只能原生拿到;直播推流(LiveKit)在拿到授权后再发布屏幕轨;UniApp 继续做业务壳。
若坚持 WebView + getDisplayMedia,在手机端几乎必然失败;把会议室下沉到 UTS/Kotlin,用官方 ScreenCaptureParams 接 MediaProjection,再用 mediaProjection 前台服务保活,才是符合 Android 平台约束、且能和对端 H5/PC 互通的做法。