系统画中画(PiP)方案说明

linf5469964@gmail.com
    分享互动规则

    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.dart
      • lib/modules/room/pip/room_system_pip_controller.dart
      • lib/modules/room/pip/room_system_pip_selector.dart
      • lib/modules/room/pages/page_room_navigation.dart
      • lib/modules/navigation/pages/page_navigation.dart
      • lib/modules/room/view-model/view_model_room_video_conference.dart
      • lib/method/native_methods.dart
      • lib/method/native_channel.dart
    • Android
      • android/app/src/main/java/com/yckj/xjl/MainActivity.java
      • android/app/src/main/java/com/yckj/xjl/SystemPipChannelHandler.java
      • android/app/src/main/AndroidManifest.xml
    • iOS
      • ios/Runner/AppDelegate.swift
      • ios/Runner/SystemPipChannelHandler.swift
      • ios/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 已含 UIBackgroundModesaudio,配合音视频通话场景。

    4. MethodChannel 协议(Flutter ↔ Native)

    Channel:com.yckj.xjl

    方法名方向语义当前端支持
    IS_SYSTEM_PIP_SUPPORTEDFlutter -> Native是否支持系统 PiPAndroid/iOS
    PREPARE_SYSTEM_PIPFlutter -> Native预初始化 PiP 资源Android/iOS
    ENTER_SYSTEM_PIPFlutter -> Native进入 PiPAndroid/iOS
    STOP_SYSTEM_PIPFlutter -> Native主动关闭 PiPAndroid/iOS
    RELEASE_SYSTEM_PIPFlutter -> Native释放 PiP 资源Android/iOS
    UPDATE_SYSTEM_PIP_USERFlutter -> Native更新 PiP 展示用户(头像/昵称等)iOS 已实现,Android 未实现

    说明:Flutter 侧 NativeChannel.invoke 会捕获 PlatformException 并返回 null,因此端能力不对齐时通常不会崩溃,但会造成功能降级(例如 Android 侧用户信息不更新)。


    5. 业务主流程(图文)

    5.1 入房后预热(Prepare)

    房间页面加载完成后,在 onLoadingComplete 中执行:

    1. _systemPipController.init():订阅音量事件(用于选取 PiP 目标用户)
    2. _systemPipController.prepareNativePip():尽早让原生完成 PiP 预创建,避免首次切后台才创建导致失败

    5.2 App 切后台(Pause)

    RoomNavigationPage.onAppPause() 中:

    1. 获取当前房间用户列表
    2. 调用 tryEnterOnAppPaused
      • 基于音量和视频状态选择展示用户
      • 调用 UPDATE_SYSTEM_PIP_USER(当前对 iOS 生效)
      • 调用 ENTER_SYSTEM_PIP(兜底触发)
    3. 根据 shouldKeepCameraOnPause() 判断:
      • 支持 PiP:保留摄像头,避免 PiP 黑屏
      • 不支持 PiP:关闭摄像头,降低后台资源

    5.3 App 回前台(Resume)

    NavigationPage.onAppResume() 中:

    1. RoomSystemPipService.instance.stopPip() 主动关闭小窗
    2. 房间逻辑恢复(重开摄像头等由房间页处理)

    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. 后续优化建议

    1. 补齐 Android UPDATE_SYSTEM_PIP_USER
      • 若 Android 侧计划展示自定义 PiP overlay 信息,需新增方法处理并落地 UI/参数更新。
    2. 统一 PiP 状态回传
      • 原生通过 EventChannel/MethodCallback 反向通知 Flutter(已进入/已退出/失败原因),提升可观测性。
    3. 补齐自动化回归
      • 增加生命周期 + PiP 能力的集成测试脚本(至少覆盖 Prepare/Enter/Stop/Release)。
    4. 文档联动
      • 每次改动 MethodChannel 协议时同步更新本文档中的方法矩阵和时序图。

    13. 结论

    当前方案采用「Flutter 业务编排 + 双端原生 PiP 控制器」结构,并通过「入房预热 + 退后台双保险触发」提升成功率。
    其中 iOS 已具备完整的用户卡片更新能力;Android 进入/退出链路稳定,但用户信息更新链路仍存在端能力缺口,建议作为下一步统一项处理。

    评论 0

    支持 @用户名 提醒对方(需为站内已注册用户名);回复仅支持一层楼中楼。

    登录后发表评论、回复与 @ 提及。

    举报

    举报会匿名发送给管理员审核。

    • 方木先生 · 2026/4/8 10:04:42

      @linf5469964@gmail.com 来看这篇文章

    • 方木先生 · 2026/4/8 10:00:30

      你好啊楼主

    码谱 · The Digital Atelier · 技术内容社区