画中画

  • Flutter

更新时间不可用

了解实时互动 RTC 在不同平台上的画中画方式,包括接入流程、关键配置和平台差异。

在声网 RTC SDK 的画中画(Picture in Picture)功能中,应用在进入后台或用户切换到其他应用时,可以将当前视频内容以悬浮小窗形式持续展示在屏幕上。用户可以在不中断通话或观看的情况下继续进行其他操作,从而提升多任务体验与用户留存。

应用场景

场景描述
视频通话在视频通话过程中,用户切换到其他应用或返回桌面时,通话画面以画中画小窗形式持续显示,保证沟通不中断,适用于社交通话、远程会议及在线客服等场景。
在线教育学员在观看老师视频讲解时,可以同时查看学习资料、记笔记或操作其他应用,提升学习效率与多任务体验。
互动直播用户观看直播时可同时浏览商品、聊天或使用其他应用,直播画面通过画中画持续播放,常见于电商直播与内容直播场景。
屏幕共享在远程协作或演示过程中,即使用户切换应用,仍可通过画中画窗口持续查看共享内容,适用于远程办公、技术支持和产品演示。
表格内容超出当前宽度,可横向滚动查看更多列。

前提条件

在开始实现前,请确保你已满足以下条件:

  • Flutter 版本为 3.7.0 或以上。
  • 项目已集成声网 RTC Flutter SDK v6.6.2 或以上版本。
  • 项目已实现基本的实时音视频功能。详见实现音视频互动。
  • 如需在 Android 设备上使用画中画,设备系统版本需为 Android 8.0 或以上。
  • 如需在 iOS 设备上使用画中画,设备系统版本需为 iOS 15.0 或以上。

实现方法

自声网 RTC Flutter SDK v6.6.2 起,声网提供 AgoraPipController 控制器及相关配置项,用于在 Android 和 iOS 平台启用和管理画中画能力。本节介绍如何在 Flutter 项目中实现画中画功能。

完成平台端配置

  • 在 Android 项目中声明当前 Activity 支持画中画模式:
XML
<activity
    android:name=".MainActivity"
    android:supportsPictureInPicture="true"
    android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|uiMode"
    android:exported="true"
    android:launchMode="singleTop">
</activity>
  • MainActivity 需继承 AgoraPIPFlutterActivity 或 AgoraPIPFlutterFragmentActivity:
Kotlin
import io.agora.agora_rtc_ng.AgoraPIPFlutterActivity

class MainActivity : AgoraPIPFlutterActivity()

信息

根据 Android 版本不同,进入画中画的方式有所区别:

  • Android 12 及以上:系统支持在用户离开当前页面的合适场景下自动进入画中画。你可以结合 autoEnterEnabled 使用。
  • Android 12 以下:系统不支持通过 autoEnterEnabled 自动进入画中画,通常需要显式触发进入画中画。

如果 MainActivity 继承 AgoraPIPFlutterActivity 或 AgoraPIPFlutterFragmentActivity,SDK 会接管相关系统回调的适配逻辑。

在 Xcode 中为 App 添加后台播放能力:

  1. 打开目标工程的 Signing & Capabilities。
  2. 点击 + Capability。
  3. 添加 Background Modes。
  4. 勾选 Audio, AirPlay, and Picture in Picture。

信息

如果你的业务需要开启在多任务模式下采集本地摄像头画面的功能,请参考如何开启 iOS 多任务采集。

获取画中画控制器、注册监听器

初始化 RTC 引擎后,调用 createPipController() 获取画中画控制器,并注册状态监听器:

Dart
late final RtcEngine _engine;
late final AgoraPipController _pip;

_engine = createAgoraRtcEngine();
await _engine.initialize(RtcEngineContext(appId: appId));

_pip = _engine.createPipController();
await _pip.registerPipStateChangedObserver(
  AgoraPipStateChangedObserver(
    onPipStateChanged: (state, error) {
      debugPrint('[PiP] state=$state error=$error');
    },
  ),
);

检查设备支持能力

检查当前设备及系统是否支持相关能力:

Dart
final pipSupported = await _pip.pipIsSupported();
final pipAutoEnterSupported = await _pip.pipIsAutoEnterSupported();

其中:

  • pipIsSupported() 用于检查当前设备是否支持画中画。
  • pipIsAutoEnterSupported() 用于检查当前设备和系统是否支持自动进入画中画。

配置画中画参数

调用 pipSetup() 配置画中画参数。你可以通过 AgoraPipOptions 为 Android 和 iOS 分别设置不同选项。

Android 平台通常需要配置宽高比、初始区域以及无缝缩放等参数:

Dart
final options = AgoraPipOptions(
  autoEnterEnabled: pipAutoEnterSupported,
  aspectRatioX: 16,
  aspectRatioY: 9,
  sourceRectHintLeft: 0,
  sourceRectHintTop: 0,
  sourceRectHintRight: 0,
  sourceRectHintBottom: 0,
  seamlessResizeEnabled: true,
  useExternalStateMonitor: false,
  externalStateMonitorInterval: 100,
);

await _pip.pipSetup(options);

信息

如果 MainActivity 已按前文完成画中画相关 Activity 适配,但在部分机型或场景下 PiP 状态回调同步不稳定,可在配置中将 useExternalStateMonitor 设为 true,并通过 externalStateMonitorInterval 配置轮询间隔,以补充画中画状态同步。

iOS 平台支持设置画中画窗口尺寸、视频流布局和系统控制样式:

Dart
final options = AgoraPipOptions(
  preferredContentWidth: 960,
  preferredContentHeight: 540,
  sourceContentView: 0,
  contentView: 0,
  contentViewLayout: const AgoraPipContentViewLayout(
    padding: 0,
    spacing: 2,
    row: 1,
    column: 2,
  ),
  videoStreams: [
    AgoraPipVideoStream(
      connection: RtcConnection(
        channelId: channelId,
        localUid: localUid,
      ),
      canvas: const VideoCanvas(
        uid: 0,
        sourceType: VideoSourceType.videoSourceCamera,
        setupMode: VideoViewSetupMode.videoViewSetupAdd,
        renderMode: RenderModeType.renderModeHidden,
        mirrorMode: VideoMirrorModeType.videoMirrorModeEnabled,
      ),
    ),
    ...remoteUsers.entries.map(
      (entry) => AgoraPipVideoStream(
        connection: entry.value,
        canvas: VideoCanvas(
          uid: entry.key,
          sourceType: VideoSourceType.videoSourceRemote,
          setupMode: VideoViewSetupMode.videoViewSetupAdd,
          renderMode: RenderModeType.renderModeHidden,
        ),
      ),
    ),
  ],
  controlStyle: 2,
);

await _pip.pipSetup(options);

在该示例中:

  • 当 contentView 设为 0 时,SDK 负责管理画中画窗口中的原生视图。若传入非 0 值,则需要自行管理原生视图布局。
  • videoStreams 用于定义需要展示的本地流和远端流。
  • contentViewLayout 用于配置视频流在画中画窗口内的流式布局,视频流从左到右、从上到下依次排列。
  • controlStyle: 2 表示隐藏播放/暂停和进度条,仅保留必要的窗口控制按钮,适合实时互动场景。

监听画中画状态变化

你可以通过实现 AgoraPipStateChangedObserver 监听画中画状态变化,并根据 AgoraPipState 更新 UI 或执行异常处理逻辑。

Dart
await _pip.registerPipStateChangedObserver(
  AgoraPipStateChangedObserver(
    onPipStateChanged: (state, error) async {
      if (state == AgoraPipState.pipStateFailed) {
        await _pip.pipDispose();
      }
    },
  ),
);

AgoraPipState 包括以下几种状态:

  • pipStateStarted:画中画已成功启动。
  • pipStateStopped:画中画已停止。
  • pipStateFailed:画中画启动失败或运行中出现错误。

启动、停止并释放画中画

完成配置后,调用 pipStart() 启动画中画。不同平台停止和释放画中画的推荐方式如下:

Dart
// 启动画中画
await _pip.pipStart();

// 释放当前画中画会话
await _pip.pipDispose();

// 销毁控制器
await _pip.dispose();

信息

Android 上通常不将 pipStop() 作为“退出画中画并恢复前台页面”的常规路径。若不再需要当前画中画会话,可调用 pipDispose() 释放资源;恢复全屏通常由用户通过系统画中画窗口操作完成。

iOS 上可调用 pipStop() 停止画中画并恢复应用界面;如果页面即将销毁或不再使用该能力,再调用 pipDispose() 和 dispose() 释放资源。

Dart
// 启动画中画
await _pip.pipStart();

// 停止画中画并恢复应用界面
await _pip.pipStop();

// 释放当前画中画会话
await _pip.pipDispose();

// 销毁控制器
await _pip.dispose();

开发注意事项

  • Android 平台上,进入画中画后通常需要根据状态调整页面 UI;许多应用会在画中画激活时仅保留视频区域。
  • Android 12 及以上系统支持应用在进入后台时自动进入画中画;更早版本通常需要在生命周期中显式调用 pipStart()。
  • pipStart() 不保证在所有时机都能成功触发,通常在应用进入 inactive 状态时调用更稳定。
  • 画中画不会绕过 Android 的后台限制。如果需要在后台持续采集或播放音视频,请结合前台服务完成适配;否则在部分 Android 版本上可能出现切后台后采集或播放中断的问题。详见Android 后台限制说明。
  • Android 上 pipStop() 的行为通常是将任务移至后台,而不是退出画中画并恢复当前页面;恢复全屏通常由用户通过系统画中画窗口操作完成。
  • 请区分会话级释放和控制器级释放:结束当前画中画会话时调用 pipDispose();页面销毁、离开频道且不再复用画中画控制器时,再调用 dispose()。
  • iOS 平台上,画中画窗口由系统和 SDK 管理,业务层通常无需额外绘制悬浮窗。
  • iOS 平台上,如果需要在画中画模式下显示多路视频流,建议通过 videoStreams 和 contentViewLayout 明确配置布局。
  • 远端用户加入或离开时,可以直接用更新后的 videoStreams 再次调用 pipSetup(),无需先调用 pipDispose()。
  • controlStyle 提供 4 种控制样式,其中 2 更适合视频通话类场景;若设为 3,系统会隐藏全部控件,包括关闭和恢复按钮。
  • 请区分会话级释放和控制器级释放:结束当前画中画会话时调用 pipDispose();页面销毁、离开频道且不再复用画中画控制器时,再调用 dispose()。
  • 在 iOS 上,仅允许由用户明确操作触发画中画(如点击按钮,上滑回到桌面)。通过代码主动调用或任何非用户触发行为,可能导致 App Store 审核被拒。

参考信息

API 参考

示例项目

声网提供了画中画功能的示例项目供你参考。你可以前往下载或查看其中的源代码。