注意:推荐使用更高效的 AI 集成助手
注意:我们为你提供了全新的 AI 集成方式,只需要描述你的需求,即可自动生成集成代码,大幅提升开发效率。
注意:点击这里,立即体验 AI 集成
一、核心功能
AtomicXCore 中用于搭建多人音视频通话场景所需要使用到的核心模块包含以下三个:
| 模块 | 功能描述 |
| CallCoreView | 通话视图核心组件。自动监听 CallStore 数据并完成画面渲染,同时支持 1v1 和多人通话布局自动切换。 |
| CallStore | 通话生命周期管理:拨打电话、接通电话、拒接电话、挂断电话。实时获取参与通话人员音视频状态,通话计时、通话记录等数据。 |
| DeviceStore | 音视频设备控制:麦克风(开关 / 音量)、摄像头(开关 / 切换 / 画质)、屏幕共享,设备状态实时监听。 |
三、步骤1:开通服务
请参见 开通服务,获取体验版或付费版 SDK。
四、步骤2:集成 SDK
安装组件:在工程的根目录下,通过命令行执行以下命令安装 atomic_x_core 插件。
flutter pub add atomic_x_core
六、Android 配置
1. 由于 SDK 内部使用了 Java 反射机制(或特性),需要将部分 SDK 类加入不进行混淆处理的名单。
·请在工程的 android/app/目录下找到 build.gradle.kts(或 build.gradle )文件中配置并开启混淆规则:
build.gradle.ktsbuild.gradleandroid { buildTypes { release { isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" ) } }}android { buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } }}
·在工程的 android/app 目录下创建 proguard-rules.pro 文件,并在其中添加如下代码:
-keep class com.tencent.** { *; }
2. (可选)如果你需要在应用外使用 CallKit 的悬浮窗能力,需要开启系统画中画特性。
请在 App 主工程的 AndroidManifest.xml 里设置 MainActivity 的 android:supportsPictureInPicture 为 true:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <application> <activity android:name=".MainActivity" android:supportsPictureInPicture="true" </activity> </application></manifest>
七、iOS 配置
由于 tencent_rtc_sdk 通过 Flutter FFI 调用接口,iOS Release 构建时 Xcode 的符号裁剪优化可能误移除 TRTC 的 C 符号,引发 symbol not found 错误。解决方案如下:
1. 在项目的 Build Settings 中找到 Deployment Postprocessing,将其设置为 Yes 。
2. 在项目的 Build Settings 中找到 Strip Style,将其中 Release 的值设置为 Non-Global Symbols 。
八、Flutter 初始化与登录流程
启动通话服务需依次完成以下三步:
1. 实例化 CallStore:CallStore.shared 初始化通话数据层。
2. 用户登录:调用 LoginStore.shared.login() 完成用户登录。
3. 初始化通话引擎:登录成功后调用 TUICallEngine.instance.init() 初始化通话信令引擎。
流程图与示例代码如下:
import 'package:atomic_x_core/atomicxcore.dart';import 'package:rtc_room_engine/api/call/tui_call_engine.dart';Future<void> _login() async { int sdkAppId = 1400000001; // 替换为你的 SDKAppID String userId = 'test_001'; // 替换为你的 UserID String userSig = 'xxxxxxxxxxx'; // 替换为你的 UserSig CallStore.shared; final result = await LoginStore.shared.login(sdkAppId, userId, userSig); TUICallEngine.instance.init(sdkAppId, userId, userSig); if (result.isSuccess) { // 登录成功 debugPrint('login success'); } else { // 登录失败 debugPrint('login failed, code: ${result.code}, message: ${result.message}'); }}
| 参数 | 类型 | 说明 |
| userId | String | 当前用户的唯一 ID,仅包含英文字母、数字、连字符和下划线。为避免多端登录冲突,请勿使用 1、123 等简单 ID。 |
| sdkAppId | int | 从 控制台 获取,通常是以 140 或 160 开头的 10 位整数。 |
| userSig | String | 用于腾讯云鉴权的票据。请注意:开发环境:你可以采用本地 GenerateTestUserSig.genTestUserSig 函数生成 userSig 或者通过 UserSig 辅助工具 生成临时的 UserSig。生产环境:为了防止密钥泄露,请务必采用服务端生成 UserSig 的方式。详细信息请参考 服务端生成 UserSig。更多信息请参见 如何计算及使用 UserSig。 |
九、实现步骤
发起通话前,请确保完成登录,这是服务可用的必要前提。以下将分 5 步为你讲解如何"拨打一通电话"。
十、步骤1:创建通话界面
你需要创建一个通话页面,当发起通话唤起通话页面时,实现方式如下:
1. 创建通话页面:你可以新建一个 StatefulWidget 作为通话宿主页面。
2. 通话页面使用 CallCoreView 组件:通话视图核心组件,需要传入 controller 参数,自动监听 CallStore 数据并完成画面渲染,支持 1v1 和多人通话布局自动切换。
import 'package:flutter/material.dart';import 'package:atomic_x_core/atomicxcore.dart';// 1. 创建通话页面 Widgetclass CallPage extends StatefulWidget { const CallPage({super.key}); @override State<CallPage> createState() => _CallPageState();}class _CallPageState extends State<CallPage> { late CallCoreController controller; @override void initState() { super.initState(); controller = CallCoreController.create(); } @override Widget build(BuildContext context) { // 2. 通话页面使用 CallCoreView Widget return CallCoreView(controller: controller); }}
CallCoreView Widget 功能说明:
| 功能 | 说明 | 参考文档 |
| 设置布局模式 | 支持自由切换布局模式。若未设置,将根据通话人数自动适配布局。 | 切换布局模式 |
| 设置头像 | 支持通过传入头像资源路径,为特定用户自定义头像。 | 自定义默认头像 |
| 设置音量提示图标 | 支持根据不同音量等级,配置个性化的音量指示图标。 | 自定义音量提示的图标 |
| 设置网络提示图标 | 支持根据实时网络质量,配置对应的网络状态提示图标。 | 自定义网络提示的图标 |
| 设置等待接听用户的动画 | 在多人通话场景下,支持传入 GIF 图像路径,为待接听状态的用户展示动画。 | 自定义 loading 动画 |
十一、步骤2:添加通话控制按钮
你可以参考 DeviceStore、CallStore 提供的 API ,自定义添加你的按钮。
·DeviceStore 功能说明:麦克风(开关 / 音量)、摄像头(开关 / 切换 / 画质)、屏幕共享,设备状态实时监听。建议将对应方法绑定至按钮点击事件,并通过监听设备状态变更来实时刷新按钮的 UI 状态。
·CallStore 功能说明:接听、挂断、拒接等核心通话控制能力。建议将对应方法绑定至按钮点击事件,并监听通话状态的变化,以确保按钮显示与当前通话阶段保持同步。
·图标资源下载:按钮图标可以直接从 Github 下载。这些图标由我们的设计师专为 TUICallKit 打造,无版权风险,可放心使用。
| 图标: | | | | | | | | |
·以添加挂断、麦克风、摄像头按钮为例,实现方式如下:
1.1 创建底部栏容器:在通话页面底部创建一个容器,用于添加挂断、麦克风、摄像头按钮。
import 'package:flutter/material.dart';import 'package:atomic_x_core/atomicxcore.dart';// 创建底部控制按钮容器 Widgetclass ControlsContainer extends StatelessWidget { const ControlsContainer({super.key}); @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ // 在这里添加控制按钮 ], ); }}
1.2 添加挂断按钮:在底部工具栏容器中添加挂断按钮,在点击事件中调用 hangup 接口并销毁页面。
import 'package:atomic_x_core/atomicxcore.dart';import 'package:flutter/material.dart';// 挂断按钮 WidgetWidget buildHangupButton() { return GestureDetector( onTap: () { // 调用 hangup 接口结束通话 CallStore.shared.hangup(); }, child: Container( width: 60, height: 60, decoration: const BoxDecoration( color: Colors.red, shape: BoxShape.circle, ), child: const Icon( Icons.call_end, color: Colors.white, size: 30, ), ), );}
1.3 添加麦克风开关按钮:在底部工具栏容器中添加麦克风开关按钮,并在点击事件中调用 openLocalMicrophone 或 closeLocalMicrophone 接口。
import 'package:atomic_x_core/atomicxcore.dart';import 'package:flutter/material.dart';// 麦克风开关按钮 Widget// 使用 ValueListenableBuilder 监听麦克风状态变化Widget buildMicrophoneButton() { return ValueListenableBuilder( valueListenable: DeviceStore.shared.state.microphoneStatus, builder: (context, status, child) { final isOn = status == DeviceStatus.on; return GestureDetector( onTap: () { // 根据当前状态开启或关闭麦克风 if (isOn) { DeviceStore.shared.closeLocalMicrophone(); } else { DeviceStore.shared.openLocalMicrophone(); } }, child: Container( width: 60, height: 60, decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), shape: BoxShape.circle, ), child: Icon( isOn ? Icons.mic : Icons.mic_off, color: Colors.white, size: 30, ), ), ); }, );}
1.4 添加摄像头开关按钮:在底部工具栏容器中添加摄像头开关按钮,并在点击事件中调用 openLocalCamera 或 closeLocalCamera 接口。
import 'package:atomic_x_core/atomicxcore.dart';import 'package:flutter/material.dart';// 摄像头开关按钮 Widget// 使用 ValueListenableBuilder 监听摄像头状态变化Widget buildCameraButton() { return ValueListenableBuilder( valueListenable: DeviceStore.shared.state.cameraStatus, builder: (context, status, child) { final isOn = status == DeviceStatus.on; return GestureDetector( onTap: () { // 根据当前状态开启或关闭摄像头 if (isOn) { DeviceStore.shared.closeLocalCamera(); } else { final isFrontCamera = DeviceStore.shared.state.isFrontCamera.value; DeviceStore.shared.openLocalCamera(isFrontCamera); } }, child: Container( width: 60, height: 60, decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), shape: BoxShape.circle, ), child: Icon( isOn ? Icons.videocam : Icons.videocam_off, color: Colors.white, size: 30, ), ), ); }, );}
1.5 实时更新媒体设备按钮状态:在 Flutter 中,使用 ValueListenableBuilder 监听麦克风和摄像头的状态变化,UI 会自动更新。
import 'package:atomic_x_core/atomicxcore.dart';import 'package:flutter/material.dart';// Flutter 中使用 ValueListenableBuilder 实现状态响应式更新// 当 DeviceStore 中的状态变化时,UI 会自动重建Widget buildDeviceControlButtons() { return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ // 麦克风按钮 – 自动响应 microphoneStatus 变化 ValueListenableBuilder( valueListenable: DeviceStore.shared.state.microphoneStatus, builder: (context, status, _) { final isOn = status == DeviceStatus.on; return Text(isOn ? '关闭麦克风' : '开启麦克风'); }, ), // 摄像头按钮 – 自动响应 cameraStatus 变化 ValueListenableBuilder( valueListenable: DeviceStore.shared.state.cameraStatus, builder: (context, status, _) { final isOn = status == DeviceStatus.on; return Text(isOn ? '关闭摄像头' : '开启摄像头'); }, ), ], );}
十二、步骤3:申请麦克风/摄像头权限
建议在发起通话前,先行检测音视频权限。若权限缺失,请引导用户动态申请。实现方法如下:
1. Android 权限声明:
在 AndroidManifest.xml 文件中声明应用需要的摄像头和麦克风权限。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <!– 麦克风权限 –> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <!– 摄像头权限 –> <uses-permission android:name="android.permission.CAMERA" /></manifest>
2. iOS 权限声明:
在你的 iOS 工程的 Info.plist 文件中,在顶级 <dict> 元素下添加以下两项。
<key>NSCameraUsageDescription</key><string>CallingApp需要访问你的相机权限,开启后录制的视频才会有画面</string><key>NSMicrophoneUsageDescription</key><string>CallingApp需要访问你的麦克风权限,开启后录制的视频才会有声音</string>
3. 动态申请权限:我们推荐使用 permission_handler 插件来动态申请音视频权限。首先使用以下命令添加 permission_handler 插件,然后申请权限:
flutter pub add permission_handler
4. iOS Podfile 配置:permission_handler 在 iOS 端使用条件编译宏控制权限模块。默认所有权限宏为 0(关闭),你必须在 ios/Podfile 的 post_install 中显式开启需要的权限宏,否则权限请求将直接返回 denied 而不会弹出系统授权弹窗。
打开 ios/Podfile,修改 post_install 部分:
post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ '$(inherited)', 'PERMISSION_CAMERA=1', 'PERMISSION_MICROPHONE=1', ] end endend
5. 申请权限:在发起通话前调用以下代码动态申请音视频权限:
import 'package:permission_handler/permission_handler.dart';// 申请音视频权限Future<bool> requestCallPermissions() async { // 请求麦克风和摄像头权限 Map<Permission, PermissionStatus> statuses = await [ Permission.microphone, Permission.camera, ].request(); // 检查权限状态 bool micGranted = statuses[Permission.microphone]?.isGranted ?? false; bool cameraGranted = statuses[Permission.camera]?.isGranted ?? false; if (micGranted && cameraGranted) { // 权限申请成功 return true; } else { // 部分权限被拒绝,可引导用户开启权限 return false; }}
十三、步骤4:发起通话
你可以在 calls 调用成功后跳转通话界面,我们建议你根据媒体类型自动开启麦克风或摄像头获得更好的通话体验,实现方式如下:
1. 发起通话:调用 calls 发起通话。
2. 开启媒体设备:发起通话成功后开启麦克风,如果是视频通话同时开启摄像头。
3. 唤起通话页面:发起通话成功,唤起通话页面。
import 'package:atomic_x_core/atomicxcore.dart';import 'package:flutter/material.dart';// 发起通话Future<void> startCall(List<String> userIdList, CallMediaType mediaType) async { final handler = await CallStore.shared.calls(userIdList, mediaType, null); if (handler.errorCode == 0) { // 唤起通话页面 if (mounted) { Navigator.push( context, MaterialPageRoute(builder: (context) => const CallPage()), ); } } else { debugPrint('Call failed: ${handler.errorCode}, ${handler.errorMessage}'); }}
calls 接口参数详细说明:
| 参数 | 类型 | 是否必填 | 说明 |
| userIdList | List<String> | 是 | 目标用户的 userId 列表。 |
| mediaType | CallMediaType | 是 | 通话媒体类型,用于指定发起音频通话还是视频通话。CallMediaType.video:视频通话。CallMediaType.audio:语音通话。 |
| params | CallParams | 否 | 通话扩展参数,如:房间号、通话邀请超时时间等。roomId (String):房间 ID,可选参数,未指定时由服务端自动分配。timeout (Int) :呼叫超时时间(秒)。userData (String):用户自定义数据。chatGroupId (String):Chat 群组 ID,用于群组通话场景。isEphemeralCall (Boolean):是否为加密通话(不产生通话记录)。 |
十四、步骤5:结束通话
无论你调用 hangup 挂断还是对方主动结束通话,均会触发 onCallEnded 事件。 建议监听该事件,当该事件被触发(即通话结束)时,执行关闭当前界面的操作。实现方式如下:
1. 监听通话结束事件:监听 onCallEnded 事件。
2. 销毁通话页面:onCallEnded 触发后,销毁通话页面。
import 'package:atomic_x_core/atomicxcore.dart';import 'package:flutter/cupertino.dart';void addListener(BuildContext context) { CallEventListener listener = CallEventListener( onCallEnded: (callId, mediaType, reason, userId) { Navigator.of(context).pop(); } ); CallStore.shared.addListener(listener);}
onCallEnded 事件参数详细说明:
| 参数 | 类型 | 说明 |
| callId | String | 此次通话的唯一标识。 |
| mediaType | CallMediaType | 通话媒体类型,用于指定发起音频通话还是视频通话。CallMediaType.video:视频通话。CallMediaType.audio:语音通话。 |
| reason | CallEndReason | 通话结束的原因。unknown:未知原因,无法确定结束原因。hangup:正常挂断,用户主动挂断通话。reject:拒绝接听,被叫方拒绝来电。noResponse:无响应,被叫方未在超时时间内接听。offline:对方离线,被叫方不在线。lineBusy:对方忙线,被叫方正在通话中。canceled:通话取消,主叫方在对方接听前取消。otherDeviceAccepted:其他设备已接听,通话已在另一登录设备上接听。otherDeviceReject:其他设备已拒绝,通话已在另一登录设备上拒绝。endByServer:服务器结束,通话被服务器终止。 |
| userId | String | 触发结束的用户 ID 。 |
十五、运行效果
当你完成以上 5 步后,"拨打一通电话"运行效果如下:
十六、定制页面
CallCoreView 提供了完善的 UI 定制能力,支持头像及音量提示等图标的自由替换。为助力快速集成,你可以直接从 Github 下载。这些图标由我们的设计师专为 TUICallKit 打造,无版权风险,可放心使用。
十七、自定义音量提示的图标
你可以调用 CallCoreView 组件的 volumeIcons 参数设置音量大小等级不同的提示图标。
·volumeIcons 示例代码:
Widget _buildCallCoreView() { Map<VolumeLevel, Image> volumeIcons = { VolumeLevel.mute : Image.asset(''), // 每个音量等级对应的图片 }; return CallCoreView( controller: CallCoreController.create(), volumeIcons: volumeIcons, );}
·volumeIcons 参数详细说明:
| 参数 | 类型 | 是否必填 | 说明 |
| volumeIcons | Map<VolumeLevel, Image> | 否 | 音量等级与图标资源的映射表。key ( VolumeLevel ) 表示音量等级:VolumeLevel.mute:表示麦克风关闭,静音状态。VolumeLevel.low:表示音量范围 (0-25]。VolumeLevel.medium:表示音量范围 (25-50]。VolumeLevel.high:表示音量范围在 (50-75]。VolumeLevel.peak:表示音量范围在 (75-100]。Value ( Image ) 表示对应音量等级的图标资源。 |
·音量提示图标:
| 图标 | 说明 |
| | 【图标含义】音量提示图标。【推荐用法】你可以将该图标等级设置为 VolumeLevel.low 或 VolumeLevel.medium ,当用户音量大于对应等级时显示。 |
| | 【图标含义】静音图标。【推荐用法】你可以将该图标等级设置为 VolumeLevel.mute ,当该用户静音时显示。 |
十八、自定义网络提示的图标
你可以调用 CallCoreView 组件的 networkQualityIcons 参数设置不同网络状态的提示图标。
·networkQualityIcons 示例代码:
Widget _buildCallCoreView() { Map<NetworkQuality, Image> networkQualityIcons = { NetworkQuality.bad : Image.asset(''), // 每个网络质量等级对应的图片 }; return CallCoreView( controller: CallCoreController.create(), networkQualityIcons: networkQualityIcons, );}
·networkQualityIcons 参数详细说明:
| 参数 | 类型 | 是否必填 | 说明 |
| networkQualityIcons | Map<NetworkQuality, Image> | 否 | 网络质量与图标资源的映射表。Key ( NetworkQuality ) : 表示网络质量等级。NetworkQuality.unknown:未知网络状态。NetworkQuality.excellent:网络状态极佳。NetworkQuality.good:网络状态较好。NetworkQuality.poor:网络状态较差。NetworkQuality.bad:网络状态差。NetworkQuality.veryBad :网络状态极差。NetworkQuality.down :网络断开。Value ( Image ) : 对应网络状态的图标资源。 |
·网络较差的提示图标:
| 图标 | 说明 |
| | 【图标含义】网络较差的提示图标。【推荐用法】你可以将该图标等级设置为 NetworkQuality.bad、NetworkQuality.veryBad 或 NetworkQuality.down ,当网络较差时显示该图标。 |
十九、自定义默认头像
你可以调用 CallCoreView 的 defaultAvatar 参数设置用户默认头像。建议你监听响应式数据 allParticipants(所有参与通话的成员):当获取到用户头像时设置并展示;若用户未设置头像或加载失败,则显示默认头像(占位图)。
·defaultAvatar 示例代码:
Widget _buildCallCoreView() { Image defaultAvatarImage = Image.asset(''); // 默认用户头像图片 return CallCoreView( controller: CallCoreController.create(), defaultAvatar: defaultAvatarImage, );}
·defaultAvatar 接口参数详细说明:
| 参数 | 类型 | 是否必填 | 说明 |
| defaultAvatar | Image | 否 | 用户默认头像。 |
·默认头像资源:
| 图标 | 说明 |
| | 【图标含义】默认头像。【推荐用法】当用户头像加载失败或无头像时,你可以给该用户设置此默认头像。 |
20、自定义 loading 动画
你可以调用 CallCoreView 的 loadingAnimation 参数,为等待中用户设置等待动画获得更好的体验。
·loadingAnimation 示例代码:
Widget _buildCallCoreView() { Image loading = Image.asset(''); // 默认加载动画资源 return CallCoreView( controller: CallCoreController.create(), loadingAnimation: loading, );}
·loadingAnimation 接口参数详细说明:
| 参数 | 类型 | 是否必填 | 说明 |
| loadingAnimation | Image | 否 | GIF 格式图像资源。 |
·等待接听的动画:
| 图标 | 说明 |
| | 【图标含义】用户等待接听动画。【推荐用法】群组通话时设置的动画。设置后,当用户的状态为等待接听时,显示该动画。 |
21、添加通话计时提示
通话计时可通过响应式数据 activeCall 的 duration 字段实时获得,实时显示通话计时的实现方式如下:
1. 数据层订阅:订阅 CallStore.shared.state.activeCall , 建立当前活跃通话的响应式监听。
2. 绑定通话计时数据:将 activeCall.duration 字段绑定至 UI 控件。该字段为响应式数据,会自动驱动 UI 实时刷新,无需手动维护定时器。
import 'package:atomic_x_core/atomicxcore.dart';import 'package:flutter/material.dart';class TimerWidget extends StatelessWidget { final double? fontSize; final FontWeight? fontWeight; const TimerWidget({ super.key, this.fontSize, this.fontWeight, }); @override Widget build(BuildContext context) { return ValueListenableBuilder( valueListenable: CallStore.shared.state.selfInfo, builder: (context, info, child) { if (info.status == CallParticipantStatus.accept) { return ValueListenableBuilder( valueListenable: CallStore.shared.state.activeCall, builder: (context, activeCall, child) { return Text( formatDuration(activeCall.duration.toInt()), style: TextStyle( fontSize: fontSize, fontWeight: fontWeight, ), ); }, ); } else { return Container(); } } ); } String formatDuration(int timeCount) { int hour = timeCount ~/ 3600; int minute = (timeCount % 3600) ~/ 60; String minuteShow = minute <= 9 ? "0$minute" : "$minute"; int second = timeCount % 60; String secondShow = second <= 9 ? "0$second" : "$second"; if (hour > 0) { String hourShow = hour <= 9 ? "0$hour" : "$hour"; return '$hourShow:$minuteShow:$secondShow'; } else { return '$minuteShow:$secondShow'; } }}
注意:说明:
注意:若你想了解更多通话状态响应式数据,详细可参考:CallState。
23、设置头像和昵称
通话开始前,你可以通过 setSelfInfo 方法,设置自己的昵称和头像。
·setSelfInfo 示例代码:
UserProfile profile = UserProfile( userID: "", // 你的 UserId avatarURL: "", // 头像的 URL nickname: "", // 需要设置的昵称);CompletionHandler result = await LoginStore.shared.setSelfInfo(userInfo: profile);if (result.errorCode == 0) { print("setSelfInfo success");} else { print("setSelfInfo failed");}
·setSelfInfo 接口参数详细说明:
| 参数 | 类型 | 是否必填 | 说明 |
| userProfile | UserProfile | 是 | 用户信息结构体。userID (String):用户的 ID 。avatarURL (String) : 用户头像的 URL。nickname (String) :用户的昵称。更多字段详情可参考 UserProfile。 |
| completion | CompletionHandler | 否 | 操作完成回调,用于返回接通电话的结果。 |
24、切换布局模式
你可以通过 setLayoutTemplate 接口灵活切换布局模式。若未主动配置,CallCoreView 将根据通话人数自动适配:1v1 场景下默认采用 Float 模式,多人通话场景下则自动切换为 Grid 模式。不同布局模式的说明如下:
| Float 模式 | Grid 模式 | PIP 模式 |
| | | |
| 布局逻辑:呼叫等待时全屏显示己方画面;接通后全屏显示对方画面,己方画面以悬浮小窗展示。交互特性:支持小窗拖拽移动,点击小窗可实现大小画面互换。 | 布局逻辑:所有成员画面呈网格状平铺排列成宫格模式布局,适用 2 人以上通话,支持点击放大画面功能。交互特性:支持点击特定成员画面放大查看。 | 布局逻辑:1v1 场景固定显示对方画面,多人场景:采用当前发言者(Active Speaker) 策略,自动识别并全屏展示正在说话的用户。交互特性:等待时显示自己的画面,接通后还会显示通话计时。 |
·setLayoutTemplate 示例代码:
CallCoreController controller = CallCoreController.create();CallLayoutTemplate template = CallLayoutTemplate.float;controller.setLayoutTemplate(template);
·setLayoutTemplate 接口参数详细说明:
| 参数 | 类型 | 是否必填 | 说明 |
| template | CallLayoutTemplate | 是 | CallCoreView 的布局模式。CallLayoutTemplate.float:布局逻辑:呼叫等待时全屏显示己方画面;接通后全屏显示对方画面,己方画面以悬浮小窗展示。交互特性:支持小窗拖拽移动,点击小窗可实现大小画面互换。CallLayoutTemplate.grid:布局逻辑:所有成员画面呈网格状平铺排列成宫格模式布局,适用 2 人以上通话,支持点击放大画面功能。交互特性:支持点击特定成员画面放大查看。CallLayoutTemplate.pip:布局逻辑:1v1 场景固定显示对方画面,多人场景:采用当前发言者(Active Speaker) 策略,自动识别并全屏展示正在说话的用户。交互特性:等待时显示自己的画面,接通后还会显示通话计时。 |
25、设置通话的默认超时时间
你可以在发起通话 calls 时,通过配置参数 CallParams 中的 timeout 字段来指定等待超时时间。示例代码如下:
void startCall(List<String> userIdList, CallMediaType mediaType) { CallParams params = CallParams( timeout: 30, // 设置通话等待超时时间 ); CallStore.shared.calls(userIdList, mediaType, params);}
calls 接口参数详细说明:
| 参数 | 类型 | 是否必填 | 说明 |
| userIdList | List<String> | 是 | 目标用户的 userId 列表。 |
| mediaType | CallMediaType | 是 | 通话媒体类型,用于指定发起音频通话还是视频通话。CallMediaType.video:视频通话。CallMediaType.audio:语音通话。 |
| params | CallParams | 否 | 通话扩展参数,如:房间号、通话邀请超时时间等。roomId (String):房间 ID,可选参数,未指定时由服务端自动分配。timeout (Int):呼叫超时时间(秒)。userData (String):用户自定义数据。chatGroupId (String):Chat 群组 ID,用于群组通话场景。isEphemeralCall (Boolean):是否为加密通话(不产生通话记录)。 |
26、实现应用内悬浮窗
在通话界面因页面导航(如用户返回、跳转至其他页面)而被覆盖时,在应用内生成一个可拖拽的悬浮窗。该悬浮窗需持续展示关键通话状态(如通话时长、对方信息),并提供一键返回完整通话界面的入口,从而提升多任务场景下的通话体验。
_buildPipWindowWidget() { final pipWidth = MediaQuery.of(context).size.width; final pipHeight = MediaQuery.of(context).size.height; final scale = pipWidth / originWidth; CallCoreController controller = CallCoreController.create(); controller.setLayoutTemplate(CallLayoutTemplate.pip); return Scaffold( body: SizedBox( width: pipWidth, height: pipHeight, child: Container( width: pipWidth, height: pipHeight, decoration: const BoxDecoration(color: Colors.transparent), child: MediaQuery( data: MediaQuery.of(context).copyWith( size: Size(originWidth ?? pipWidth, originHeight ?? pipHeight) ), child: ClipRect( child: Transform.scale( scale: scale, alignment: Alignment.center, child: OverflowBox( maxWidth: originWidth, maxHeight: originHeight, alignment: Alignment.center, child: CallCoreView( controller: controller, ), ), ), ), ), ), ), );}
27、实现 Android 应用外画中画
画中画功能需 Android 8.0 (API 26) 及以上版本支持。
1. MainActivity 配置
监听 MainActivity 的生命周期,当 enablePictureInPicture 为 true 时,当应用退到后台时自动拉起画中画。
import android.app.PictureInPictureParamsimport android.content.pm.PackageManagerimport android.os.Buildimport android.util.Logimport android.util.Rationalimport io.flutter.embedding.android.FlutterActivityimport io.flutter.embedding.engine.FlutterEngineimport io.flutter.plugin.common.MethodChannelclass MainActivity : FlutterActivity() { companion object { private const val TAG = "MainActivity" private const val CHANNEL = "atomic_x/pip" } private var enablePictureInPicture = false override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> when (call.method) { "enablePictureInPicture" -> { val enable = call.argument<Boolean>("enable") ?: false val success = enablePIP(enable) result.success(success) } "enterPictureInPicture" -> { val success = enterPIP() result.success(success) } else -> result.notImplemented() } } } override fun onUserLeaveHint() { super.onUserLeaveHint() // 用户按 Home 键时自动进入画中画 if (enablePictureInPicture) { enterPIP() } } private fun enablePIP(enable: Boolean): Boolean { Log.i(TAG, "enablePictureInPicture: $enable") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) { enablePictureInPicture = enable return true } return false } private fun enterPIP(): Boolean { if (!enablePictureInPicture) return false if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { try { val aspectRatio = Rational(9, 16) val params = PictureInPictureParams.Builder() .setAspectRatio(aspectRatio) .build() return enterPictureInPictureMode(params) } catch (e: Exception) { Log.e(TAG, "enterPIP failed: ${e.message}") } } return false }}
2. AndroidManifest 配置
在项目的 AndroidManifest.xml 文件中为 MainActivity 添加画中画能力配置: android:supportsPictureInPicture="true"
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" android:taskAffinity="" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize" android:supportsPictureInPicture="true"> <meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" /> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter></activity>
3. Dart 层配置
import 'package:flutter/services.dart';class PipManager { static const MethodChannel _channel = MethodChannel('atomic_x/pip'); /// 启用/禁用画中画功能 static Future<bool> enablePictureInPicture(bool enable) async { try { final result = await _channel.invokeMethod<bool>('enablePictureInPicture', {'enable': enable}); return result ?? false; } catch (e) { return false; } } /// 立即进入画中画模式 static Future<bool> enterPictureInPicture() async { try { final result = await _channel.invokeMethod<bool>('enterPictureInPicture'); return result ?? false; } catch (e) { return false; } }}
在你的通话开始前,选择合适的时机启用画中画。在通话结束后,关闭画中画。
28、实现 iOS 应用外画中画
iOS 端支持通过底层 TRTC 引擎实现应用外画中画功能。当应用进入后台时,通话画面可以以系统画中画形式悬浮在其他应用之上,用户可以边使用其他应用边进行视频通话。实现方式如下:
注意:说明:
注意:需要在 Xcode 的 Signing & Capabilities 中添加 Background Modes 能力,并勾选 Audio, AirPlay, and Picture in Picture。
注意:需要 iOS 15.0 及以上版本支持。
1. 开启画中画
import 'package:tencent_rtc_sdk/trtc_cloud.dart';TRTCCloud.sharedInstance().then((trtcCloud) { trtcCloud.callExperimentalAPI(''' { "api": "configPictureInPicture", "params": { "enable": true, "cameraBackgroundCapture": true, "canvas": { "width": 720, "height": 1280, "backgroundColor": "#111111" }, "regions": [ { "userId": "remoteUserId", "userName": "", "width": 1.0, "height": 1.0, "x": 0.0, "y": 0.0, "fillMode": 0, "streamType": "high", "backgroundColor": "#111111", "backgroundImage": "file:///path/to/avatar.png" }, { "userId": "localUserId", "userName": "", "width": 0.333, "height": 0.333, "x": 0.65, "y": 0.05, "fillMode": 0, "streamType": "high", "backgroundColor": "#111111" } ] } } ''');});
2. 关闭画中画
import 'package:tencent_rtc_sdk/trtc_cloud.dart';TRTCCloud.sharedInstance().then((trtcCloud) { trtcCloud.callExperimentalAPI(''' { "api": "configPictureInPicture", "params": { "enable": false } } ''');});
29、播放等待接听的提示音
你可以监听自己的通话状态,在等待接听时播放铃声,在接听通话或通话结束停止播放铃声。
CallStore.shared.state.selfInfo.addListener(() { CallParticipantInfo info = CallStore.shared.state.selfInfo.value; if (info.status == CallParticipantStatus.accept || info.status == CallParticipantStatus.none) { // 停止播放铃音 return; } if (info.status == CallParticipantStatus.waiting) { // 播放铃音 }});
30、开启后台采集音频/视频
为了确保应用在进入后台时仍能正常采集音频和视频(例如用户锁屏或切换到其他应用),你需要分别在 Android 、iOS 进行如下配置:
31、Android 端配置
1. 配置权限与服务(AndroidManifest.xml): 从 Android 9.0 (API 28) 开始需要声明前台服务权限;Android 14 (API 34) 强制要求声明具体的服务类型(麦克风和摄像头)。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" /> <application> <service android:name=".CallForegroundService" android:enabled="true" android:exported="false" android:foregroundServiceType="camera|microphone" /> </application></manifest>
2. 创建前台服务类(CallForegroundService):
import android.app.Notificationimport android.app.NotificationChannelimport android.app.NotificationManagerimport android.app.Serviceimport android.content.Contextimport android.content.Intentimport android.os.Buildimport android.os.IBinderimport androidx.core.app.NotificationCompatclass CallForegroundService : Service() { companion object { private const val NOTIFICATION_ID = 1001 private const val CHANNEL_ID = "call_foreground_channel" fun start(context: Context) { val intent = Intent(context, CallForegroundService::class.java) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.startForegroundService(intent) } else { context.startService(intent) } } fun stop(context: Context) { val intent = Intent(context, CallForegroundService::class.java) context.stopService(intent) } } override fun onCreate() { super.onCreate() createNotificationChannel() // 启动前台通知,确保后台采集权限 startForeground(NOTIFICATION_ID, createNotification()) } override fun onBind(intent: Intent?): IBinder? = null private fun createNotification(): Notification { return NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("正在通话中") .setContentText("应用正在后台运行以保持通话") .setSmallIcon(android.R.drawable.ic_menu_call) // 请替换为你的应用图标 .setPriority(NotificationCompat.PRIORITY_HIGH) .build() } private fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( CHANNEL_ID, "通话保活服务", NotificationManager.IMPORTANCE_HIGH ) val manager = getSystemService(NotificationManager::class.java) manager.createNotificationChannel(channel) } }}
32、iOS 端配置
配置步骤:在 Xcode 中打开你的项目,按照以下步骤操作:
1. 选择项目的 Target > Signing & Capabilities。
2. 点击 + Capability 。
3. 搜索并添加 Background Modes。
4. 勾选以下三个选项:
·Audio, AirPlay, and Picture in Picture(保持音频采集和画中画功能)。
·Voice over IP(支持 VoIP 通话)。
·Remote notifications(可选,用于接收离线推送)。
配置完成后,你的 Info.plist 文件会自动添加以下内容:
<key>UIBackgroundModes</key><array> <string>audio</string> <string>voip</string> <string>remote-notification</string></array>
·配置音频会话(AVAudioSession)
为了确保通话音频在后台正常工作,你需要在通话开始前配置音频会话。建议在通话界面的 viewDidLoad 或发起通话前设置:
import AVFoundation/** * 设置音频会话,支持后台音频采集 * * 建议在以下场景调用: * 1. 通话界面的 viewDidLoad 中 * 2. 发起通话 (calls) 之前 * 3. 接听通话 (accept) 之前 */private func start() { let audioSession = AVAudioSession.sharedInstance() do { // 设置音频会话类别为播放和录音 // .allowBluetooth: 支持蓝牙耳机 // .allowBluetoothA2DP: 支持高质量蓝牙音频(A2DP 协议) try audioSession.setCategory(.playAndRecord, options: [.allowBluetooth, .allowBluetoothA2DP]) // 激活音频会话 try audioSession.setActive(true) } catch { // 音频会话配置失败 }}
注意:你需要在应用中合适的时机,使用 MethodChannel 调用 start() 方法以开启后台保活能力。
33、下一步
恭喜你,已经完成了"拨打一通电话",接下来,你可以实现接听第一通电话功能,可参考下表:
| 功能 | 描述 | 集成指引 |
| 接听第一通电话 | 引导你快速完成接听流程集成。涵盖通话界面唤起、接听及拒接等核心控制功能。 | 接听第一通电话 |
35、iOS release 包运行时 [symbol not found]?
由于 tencent_rtc_sdk 通过 Flutter FFI 调用接口,iOS Release 构建时 Xcode 的符号裁剪优化可能误移除 TRTC 的 C 符号,引发 symbol not found 错误。解决方案如下:
1. 在项目的 Build Settings 中找到 Deployment Postprocessing,将其设置为 Yes 。
2. 在项目的 Build Settings 中找到 Strip Style,将其中 Release 的值设置为 Non-Global Symbols 。
36、在通话邀请超时时间内,被邀请者如果离线再上线,能否收到来电事件?
单人通话时,如果在超时时间内上线,会触发来电邀请;群组通话,如果在超时时间内上线,会拉起未处理的20条群消息,如果存在通话邀请,则触发来电邀请事件。
37、联系我们
如果你在使用过程中,有什么建议或者意见,可以 联系我们,感谢你的反馈。