1. 目标与范围
本文档用于沉淀当前项目「房间视频通话」的系统画中画(Picture in Picture, PiP)实现,覆盖:
- Flutter 业务层触发链路
- Android 原生 PiP 实现细节
- iOS 原生 Video Call PiP 实现细节
- 生命周期时序、流程图、状态机
- 风险点与排查建议
不覆盖:自定义悬浮窗(Overlay)方案,本方案仅讨论系统级 PiP。
2. 代码落点(图文总览)
2.1 模块分层图
flowchart TB
A[RoomNavigationPage<br/>生命周期 onAppPause/onAppResume] --> B[RoomSystemPipController<br/>PiP 业务编排]
B --> C[RoomSystemPipService<br/>NativeChannel 调用封装]
C --> D[MethodChannel: com.yckj.xjl]
D --> E1[Android MainActivity + SystemPipChannelHandler]
D --> E2[iOS AppDelegate + SystemPipChannelHandler]
B --> F[RoomSystemPipSelector<br/>目标用户选择]
B --> G[ObserverEngineEventBus<br/>音量事件]
2.2 关键文件
- Flutter
lib/modules/room/pip/service_room_system_pip.dartlib/modules/room/pip/room_system_pip_controller.dartlib/modules/room/pip/room_system_pip_selector.dartlib/modules/room/pages/page_room_navigation.dartlib/modules/navigation/pages/page_navigation.dartlib/modules/room/view-model/view_model_room_video_conference.dartlib/method/native_methods.dartlib/method/native_channel.dart
- Android
android/app/src/main/java/com/yckj/xjl/MainActivity.javaandroid/app/src/main/java/com/yckj/xjl/SystemPipChannelHandler.javaandroid/app/src/main/AndroidManifest.xml
- iOS
ios/Runner/AppDelegate.swiftios/Runner/SystemPipChannelHandler.swiftios/Runner/Info.plist
3. 原生能力与配置前置
3.1 Android
MainActivity已声明:android:supportsPictureInPicture="true"android:resizeableActivity="true"
- 能力判断:
- API >= 26(Android O)
PackageManager.FEATURE_PICTURE_IN_PICTURE
3.2 iOS
- 能力判断:
- iOS >= 15
AVPictureInPictureController.isPictureInPictureSupported()
Info.plist已含UIBackgroundModes的audio,配合音视频通话场景。
4. MethodChannel 协议(Flutter ↔ Native)
Channel:com.yckj.xjl
| 方法名 | 方向 | 语义 | 当前端支持 |
|---|---|---|---|
IS_SYSTEM_PIP_SUPPORTED | Flutter -> Native | 是否支持系统 PiP | Android/iOS |
PREPARE_SYSTEM_PIP | Flutter -> Native | 预初始化 PiP 资源 | Android/iOS |
ENTER_SYSTEM_PIP | Flutter -> Native | 进入 PiP | Android/iOS |
STOP_SYSTEM_PIP | Flutter -> Native | 主动关闭 PiP | Android/iOS |
RELEASE_SYSTEM_PIP | Flutter -> Native | 释放 PiP 资源 | Android/iOS |
UPDATE_SYSTEM_PIP_USER | Flutter -> Native | 更新 PiP 展示用户(头像/昵称等) | iOS 已实现,Android 未实现 |
说明:Flutter 侧
NativeChannel.invoke会捕获PlatformException并返回null,因此端能力不对齐时通常不会崩溃,但会造成功能降级(例如 Android 侧用户信息不更新)。
5. 业务主流程(图文)
5.1 入房后预热(Prepare)
房间页面加载完成后,在 onLoadingComplete 中执行:
_systemPipController.init():订阅音量事件(用于选取 PiP 目标用户)_systemPipController.prepareNativePip():尽早让原生完成 PiP 预创建,避免首次切后台才创建导致失败
5.2 App 切后台(Pause)
RoomNavigationPage.onAppPause() 中:
- 获取当前房间用户列表
- 调用
tryEnterOnAppPaused:- 基于音量和视频状态选择展示用户
- 调用
UPDATE_SYSTEM_PIP_USER(当前对 iOS 生效) - 调用
ENTER_SYSTEM_PIP(兜底触发)
- 根据
shouldKeepCameraOnPause()判断:- 支持 PiP:保留摄像头,避免 PiP 黑屏
- 不支持 PiP:关闭摄像头,降低后台资源
5.3 App 回前台(Resume)
NavigationPage.onAppResume() 中:
RoomSystemPipService.instance.stopPip()主动关闭小窗- 房间逻辑恢复(重开摄像头等由房间页处理)
5.4 退出房间(Release)
RoomVideoConferenceViewModel.disposeVideoViews() 中调用:
RoomSystemPipService.instance.releasePip()
释放原生控制器和资源,避免跨房间残留状态。
6. 核心时序图
6.1 入房预热时序
sequenceDiagram
participant UI as RoomNavigationPage
participant C as RoomSystemPipController
participant S as RoomSystemPipService
participant N as Native(MethodChannel)
UI->>C: init()
C->>C: 订阅 onUserVoiceVolumeChanged$
UI->>C: prepareNativePip()
C->>S: preparePip()
S->>N: PREPARE_SYSTEM_PIP
N-->>S: true/false
6.2 切后台进入 PiP 时序
sequenceDiagram
participant App as AppLifecycle(Pause)
participant UI as RoomNavigationPage
participant C as RoomSystemPipController
participant Sel as RoomSystemPipSelector
participant S as RoomSystemPipService
participant A as Android/iOS Native
App->>UI: onAppPause()
UI->>C: tryEnterOnAppPaused(users)
C->>Sel: selectTarget(users, volumes)
Sel-->>C: targetUser
C->>S: updatePipUser(target) (iOS有效)
C->>S: enterPip() (兜底)
S->>A: ENTER_SYSTEM_PIP
A-->>S: true/false
UI->>C: shouldKeepCameraOnPause()
C->>S: isSupported()
S->>A: IS_SYSTEM_PIP_SUPPORTED
A-->>S: true/false
6.3 回前台关闭 PiP 时序
sequenceDiagram
participant App as AppLifecycle(Resume)
participant Nav as NavigationPage
participant S as RoomSystemPipService
participant A as Android/iOS Native
App->>Nav: onAppResume()
Nav->>S: stopPip()
S->>A: STOP_SYSTEM_PIP
7. 目标用户选择流程图(Flutter)
规则来源:RoomSystemPipSelector
flowchart TD
A[输入 users + volumes] --> B{users 是否为空}
B -- 是 --> Z[返回 null]
B -- 否 --> C[拆分本地用户 / 远端用户]
C --> D[筛选远端说话用户 volumes>0]
D --> E{有远端说话用户吗}
E -- 否 --> F[返回本地用户]
E -- 是 --> G[筛选说话且开摄像头用户]
G --> H{该集合是否为空}
H -- 否 --> I[候选=说话且开摄像头]
H -- 是 --> J[候选=说话远端]
I --> K[按音量倒序排序]
J --> K
K --> L[返回第一名]
8. Android 方案细节
实现类:SystemPipChannelHandler.java
8.1 关键设计
roomPipSessionActive:标记房间 PiP 会话是否激活PREPARE_SYSTEM_PIP:- 置
roomPipSessionActive = true - 调
setPictureInPictureParams(...) - Android 12+ 配置
setAutoEnterEnabled(true)
- 置
onUserLeaveHint():- 由
MainActivity.onUserLeaveHint()转发 - 在用户离开前台瞬间同步触发进入 PiP(规避 Flutter 异步时机过晚)
- 由
ENTER_SYSTEM_PIP:- 显式
enterPictureInPictureMode(params),作为兜底
- 显式
STOP/RELEASE:- 关闭自动进入参数
- 若当前处于 PiP,执行
moveTaskToBack(false)收尾
8.2 Android 生命周期流程图
stateDiagram-v2
[*] --> Idle
Idle --> Prepared: PREPARE_SYSTEM_PIP
Prepared --> PipActive: onUserLeaveHint / ENTER_SYSTEM_PIP
PipActive --> BackgroundTask: STOP_SYSTEM_PIP
Prepared --> Released: RELEASE_SYSTEM_PIP
PipActive --> Released: RELEASE_SYSTEM_PIP
BackgroundTask --> Released: RELEASE_SYSTEM_PIP
Released --> [*]
9. iOS 方案细节
实现类:SystemPipChannelHandler.swift
9.1 关键设计
- 使用
AVPictureInPictureVideoCallViewController承载 PiP 内容 prepareSystemPip()中完成:ContentSource(activeVideoCallSourceView: flutterVC.view, contentViewController: ...)AVPictureInPictureController初始化与 delegate 绑定
UPDATE_SYSTEM_PIP_USER:- 更新
currentPipUserInfo - 立即刷新 PiP 预览卡片(头像/昵称)
- 更新
- 卡片 UI:
- 毛玻璃 + 深色蒙层
- 圆形头像 + 单行昵称
- 尺寸自适应(
layoutSubviews中按窗口缩放)
enterSystemPip():- 若未 prepare,先自动 prepare
startPictureInPicture()
releaseSystemPip():- 停止 PiP,解绑 delegate,清空 controller/content controller/store
9.2 iOS 视图关系示意
flowchart LR
A[FlutterViewController.view] --> B[AVPictureInPictureController.ContentSource]
C[AVPictureInPictureVideoCallViewController] --> B
C --> D[PipCardView]
D --> E[Blur + Tint]
D --> F[Avatar UIImageView]
D --> G[Name UILabel]
10. 差异点与风险清单(重点)
10.1 平台能力差异
UPDATE_SYSTEM_PIP_USER:- iOS:已实现并用于卡片内容更新
- Android:当前
SystemPipChannelHandler未处理该方法
- 影响:
- Android 不会按音量策略更新 PiP 展示用户信息(但进入 PiP 功能仍可工作)
10.2 时机风险
- 仅依赖 Flutter
onAppPause异步进 PiP,在部分 Android 机型可能晚于系统窗口,导致进入失败 - 当前已通过
onUserLeaveHint+ 兜底ENTER_SYSTEM_PIP双保险处理
10.3 生命周期一致性
- 进入房间后必须
preparePip - 离开房间必须
releasePip - 回前台应
stopPip,避免主界面与 PiP 并存
11. 联调与验收清单
11.1 功能验收
- 支持机型上,进房后按 Home 能稳定进入 PiP
- PiP 中音视频不中断,回前台可恢复主界面
- 离房后不应残留 PiP 窗口
- 多次进出房间后无重复初始化异常
11.2 场景回归
- 首次安装首次授权(相机/麦克风)
- 有远端说话人 / 无远端说话人
- 本地开关摄像头状态切换后再进 PiP
- Android 12+ 自动进入行为与手动进入行为一致
11.3 日志建议
- Android 关键 tag:
SystemPiP - iOS 关键日志:
[SystemPiP] ... - Flutter 关键日志:
RoomSystemPipController: ...调用原生方法 xxx 失败
12. 后续优化建议
- 补齐 Android
UPDATE_SYSTEM_PIP_USER- 若 Android 侧计划展示自定义 PiP overlay 信息,需新增方法处理并落地 UI/参数更新。
- 统一 PiP 状态回传
- 原生通过 EventChannel/MethodCallback 反向通知 Flutter(已进入/已退出/失败原因),提升可观测性。
- 补齐自动化回归
- 增加生命周期 + PiP 能力的集成测试脚本(至少覆盖 Prepare/Enter/Stop/Release)。
- 文档联动
- 每次改动 MethodChannel 协议时同步更新本文档中的方法矩阵和时序图。
13. 结论
当前方案采用「Flutter 业务编排 + 双端原生 PiP 控制器」结构,并通过「入房预热 + 退后台双保险触发」提升成功率。
其中 iOS 已具备完整的用户卡片更新能力;Android 进入/退出链路稳定,但用户信息更新链路仍存在端能力缺口,建议作为下一步统一项处理。