画中画
在声网 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
- iOS
- 在 Android 项目中声明当前 Activity 支持画中画模式:
<activity
android:name=".MainActivity"
android:supportsPictureInPicture="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|uiMode"
android:exported="true"
android:launchMode="singleTop">
</activity>
MainActivity需继承AgoraPIPFlutterActivity或AgoraPIPFlutterFragmentActivity:
import io.agora.agora_rtc_ng.AgoraPIPFlutterActivity
class MainActivity : AgoraPIPFlutterActivity()
根据 Android 版本不同,进入画中画的方式有所区别:
- Android 12 及以上:系统支持在用户离开当前页面的合适场景下自动进入画中画。你可以结合
autoEnterEnabled使用。 - Android 12 以下:系统不支持通过
autoEnterEnabled自动进入画中画,通常需要显式触发进入画中画。
如果 MainActivity 继承 AgoraPIPFlutterActivity 或 AgoraPIPFlutterFragmentActivity,SDK 会接管相关系统回调的适配逻辑。
在 Xcode 中为 App 添加后台播放能力:
- 打开目标工程的 Signing & Capabilities。
- 点击 + Capability。
- 添加 Background Modes。
- 勾选 Audio, AirPlay, and Picture in Picture。
如果你的业务需要开启在多任务模式下采集本地摄像头画面的功能,请参考如何开启 iOS 多任务采集。
获取画中画控制器、注册监听器
初始化 RTC 引擎后,调用 createPipController() 获取画中画控制器,并注册状态监听器:
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');
},
),
);
检查设备支持能力
检查当前设备及系统是否支持相关能力:
final pipSupported = await _pip.pipIsSupported();
final pipAutoEnterSupported = await _pip.pipIsAutoEnterSupported();
其中:
pipIsSupported()用于检查当前设备是否支持画中画。pipIsAutoEnterSupported()用于检查当前设备和系统是否支持自动进入画中画。
配置画中画参数
调用 pipSetup() 配置画中画参数。你可以通过 AgoraPipOptions 为 Android 和 iOS 分别设置不同选项。
- Android
- iOS
Android 平台通常需要配置宽高比、初始区域以及无缝缩放等参数:
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 平台支持设置画中画窗口尺寸、视频流布局和系统控制样式:
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 或执行异常处理逻辑。
await _pip.registerPipStateChangedObserver(
AgoraPipStateChangedObserver(
onPipStateChanged: (state, error) async {
if (state == AgoraPipState.pipStateFailed) {
await _pip.pipDispose();
}
},
),
);
AgoraPipState 包括以下几种状态:
pipStateStarted:画中画已成功启动。pipStateStopped:画中画已停止。pipStateFailed:画中画启动失败或运行中出现错误。
启动、停止并释放画中画
完成配置后,调用 pipStart() 启动画中画。不同平台停止和释放画中画的推荐方式如下:
- Android
- iOS
// 启动画中画
await _pip.pipStart();
// 释放当前画中画会话
await _pip.pipDispose();
// 销毁控制器
await _pip.dispose();
Android 上通常不将 pipStop() 作为“退出画中画并恢复前台页面”的常规路径。若不再需要当前画中画会话,可调用 pipDispose() 释放资源;恢复全屏通常由用户通过系统画中画窗口操作完成。
iOS 上可调用 pipStop() 停止画中画并恢复应用界面;如果页面即将销毁或不再使用该能力,再调用 pipDispose() 和 dispose() 释放资源。
// 启动画中画
await _pip.pipStart();
// 停止画中画并恢复应用界面
await _pip.pipStop();
// 释放当前画中画会话
await _pip.pipDispose();
// 销毁控制器
await _pip.dispose();
开发注意事项
- Android
- iOS
- 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 参考
-
AgoraPipController -
AgoraPipOptions -
AgoraPipContentViewLayout -
AgoraPipVideoStream -
pipIsSupported -
pipIsAutoEnterSupported -
pipSetup -
pipStart -
pipStop -
pipDispose -
registerPipStateChangedObserver -
AgoraPipStateChangedObserver -
AgoraPipState -
onPipStateChanged
示例项目
声网提供了画中画功能的示例项目供你参考。你可以前往下载或查看其中的源代码。