製品アップデート情報
Tencent Cloudオーディオビデオ端末SDKの再生アップグレードおよび承認チェック追加に関するお知らせ
TRTCアプリケーションのサブスクリプションパッケージサービスのリリースに関する説明について

Module | Description |
Core UI component for calls. Automatically observes CallStore data and renders the interface, switching layouts between 1v1 and group calls as needed. | |
Manages the call lifecycle: make, answer, reject, hang up. Provides real-time status for participants’ audio/video, call duration, call history, and more. | |
Controls audio/video devices: microphone (toggle/volume), camera (toggle/switch/quality), screen sharing, and monitors device status in real time. |
flutter pub add atomic_x_core
android/app/ directory, update build.gradle.kts or build.gradle to enable Proguard rules:android {buildTypes {release {isMinifyEnabled = trueproguardFiles(getDefaultProguardFile("proguard-android.txt"),"proguard-rules.pro")}}}
android {buildTypes {release {minifyEnabled trueproguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'}}}
proguard-rules.pro file in android/app and add:-keep class com.tencent.** { *; }
MainActivity in AndroidManifest.xml:<manifest xmlns:android="http://schemas.android.com/apk/res/android"><application><activityandroid:name=".MainActivity"android:supportsPictureInPicture="true"</activity></application></manifest>
symbol not found error in Release on iOS (due to Flutter FFI symbol stripping), do the following:

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; // Enter your SDKAppIDString userId = 'test_001'; // Enter your UserIDString userSig = 'xxxxxxxxxxx'; // Enter your UserSigCallStore.shared;final result = await LoginStore.shared.login(sdkAppId, userId, userSig);TUICallEngine.instance.init(sdkAppId, userId, userSig);if (result.isSuccess) {// Login successfuldebugPrint('login success');} else {// Login faileddebugPrint('login failed, code: ${result.code}, message: ${result.message}');}}
Parameter | Type | Description |
userId | String | Unique identifier for the current user; use only letters, numbers, hyphens, and underscores. Avoid simple IDs like 1 or 123 to prevent multi-device login conflicts. |
sdkAppId | int | |
userSig | String | Authentication token for TRTC. Development: Generate userSig locally using GenerateTestUserSig.genTestUserSig or with the UserSig Assistant Tool for temporary use. Production: Always generate userSig on your server to protect your secret key. See Server-side UserSig generation. |
StatefulWidget for your call host page, which will be used for navigation on incoming calls.controller parameter, observes CallStore data, and automatically adjusts the layout for 1v1 or multi-party calls.import 'package:flutter/material.dart';import 'package:atomic_x_core/atomicxcore.dart';// 1. Create call page Widgetclass CallPage extends StatefulWidget {const CallPage({super.key});@overrideState<CallPage> createState() => _CallPageState();}class _CallPageState extends State<CallPage> {late CallCoreController controller;@overridevoid initState() {super.initState();controller = CallCoreController.create();}@overrideWidget build(BuildContext context) {// 2. Use CallCoreView Widget on the call pagereturn CallCoreView(controller: controller);}}
Feature | Description | Reference |
Set layout mode | Supports dynamic layout mode switching. If not set, layout auto-adapts by participant count. | |
Set avatar | Allows custom avatars for users by passing resource paths. | |
Set volume indicator icon | Custom icons for different volume levels. | |
Set network indicator icon | Custom icons for real-time network quality. | |
Set waiting animation | Displays GIF animation for users in a waiting state during multi-party calls. |
Icons: | ![]() | ![]() | ![]() | ![]() | ![]() | ![]() | ![]() | ![]() |
Download links: |
import 'package:flutter/material.dart';import 'package:atomic_x_core/atomicxcore.dart';// Bottom control button container Widgetclass ControlsContainer extends StatelessWidget {const ControlsContainer({super.key});@overrideWidget build(BuildContext context) {return Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly,children: [// Add control buttons here],);}}
hangup to end the call and close the page.import 'package:atomic_x_core/atomicxcore.dart';import 'package:flutter/material.dart';// Hang up button WidgetWidget buildHangupButton() {return GestureDetector(onTap: () {// Call hangup API to end the callCallStore.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,),),);}
openLocalMicrophone and closeLocalMicrophone to toggle the mic.import 'package:atomic_x_core/atomicxcore.dart';import 'package:flutter/material.dart';// Microphone toggle button Widget// Use ValueListenableBuilder to listen for microphone status changesWidget buildMicrophoneButton() {return ValueListenableBuilder(valueListenable: DeviceStore.shared.state.microphoneStatus,builder: (context, status, child) {final isOn = status == DeviceStatus.on;return GestureDetector(onTap: () {// Toggle microphone based on current statusif (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,),),);},);}
openLocalCamera and closeLocalCamera to toggle the camera.import 'package:atomic_x_core/atomicxcore.dart';import 'package:flutter/material.dart';// Camera toggle button Widget// Use ValueListenableBuilder to listen for camera status changesWidget buildCameraButton() {return ValueListenableBuilder(valueListenable: DeviceStore.shared.state.cameraStatus,builder: (context, status, child) {final isOn = status == DeviceStatus.on;return GestureDetector(onTap: () {// Toggle camera based on current statusif (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,),),);},);}
ValueListenableBuilder to react to device status changes.import 'package:atomic_x_core/atomicxcore.dart';import 'package:flutter/material.dart';// In Flutter, use ValueListenableBuilder for reactive state updates// When DeviceStore state changes, UI rebuilds automaticallyWidget buildDeviceControlButtons() {return Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly,children: [// Microphone button - reacts to microphoneStatus changesValueListenableBuilder(valueListenable: DeviceStore.shared.state.microphoneStatus,builder: (context, status, _) {final isOn = status == DeviceStatus.on;return Text(isOn ? 'Turn off microphone' : 'Turn on microphone');},),// Camera button - reacts to cameraStatus changesValueListenableBuilder(valueListenable: DeviceStore.shared.state.cameraStatus,builder: (context, status, _) {final isOn = status == DeviceStatus.on;return Text(isOn ? 'Turn off camera' : 'Turn on camera');},),],);}
AndroidManifest.xml:<manifest xmlns:android="http://schemas.android.com/apk/res/android"><!-- Microphone permission --><uses-permission android:name="android.permission.RECORD_AUDIO" /><!-- Camera permission --><uses-permission android:name="android.permission.CAMERA" /></manifest>
Info.plist:<key>NSCameraUsageDescription</key><string>CallingApp needs access to your camera. Video recording requires camera permission.</string><key>NSMicrophoneUsageDescription</key><string>CallingApp needs access to your microphone. Video recording requires microphone permission.</string>
permission_handler plugin for runtime permission requests.flutter pub add permission_handler
import 'package:permission_handler/permission_handler.dart';// Request audio/video permissionsFuture<bool> requestCallPermissions() async {// Request microphone and camera permissionsMap<Permission, PermissionStatus> statuses = await [Permission.microphone,Permission.camera,].request();// Check permission statusbool micGranted = statuses[Permission.microphone]?.isGranted ?? false;bool cameraGranted = statuses[Permission.camera]?.isGranted ?? false;if (micGranted && cameraGranted) {// Permissions grantedreturn true;} else {// Some permissions denied, prompt user to enablereturn false;}}
calls to start.import 'package:atomic_x_core/atomicxcore.dart';import 'package:flutter/material.dart';// Initiate callFuture<void> startCall(List<String> userIdList, CallMediaType mediaType) async {final handler = await CallStore.shared.calls(userIdList, mediaType, null);if (handler.errorCode == 0) {// Navigate to call pageif (mounted) {Navigator.push(context,MaterialPageRoute(builder: (context) => const CallPage()),);}} else {debugPrint('Call failed: ${handler.errorCode}, ${handler.errorMessage}');}}
Parameter | Type | Required | Description |
userIdList | List | Yes | List of target users’ userId. |
mediaType | Yes | Specifies call type: audio or video. CallMediaType.video: Video callCallMediaType.audio: Audio call | |
params | No | Additional parameters: room ID, timeout, custom data, group ID, ephemeral flag. |
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);}
Parameter | Type | Description |
callId | String | Unique identifier for the call. |
mediaType | Specifies audio or video call. CallMediaType.video: VideoCallMediaType.audio: Audio | |
reason | Reason for call end: unknown: Unknownhangup: User hung upreject: Call rejectednoResponse: No answeroffline: Callee offlinelineBusy: Callee busycanceled: Caller canceledotherDeviceAccepted: Answered elsewhereotherDeviceReject: Rejected elsewhereendByServer: Ended by server | |
userId | String | User ID that triggered the end event |


Widget _buildCallCoreView() {Map<VolumeLevel, Image> volumeIcons = {VolumeLevel.mute : Image.asset(''), // Icon for mute};return CallCoreView(controller: CallCoreController.create(),volumeIcons: volumeIcons,);}
Parameter | Type | Required | Description |
volumeIcons | Map | No | Maps volume levels to icons. VolumeLevel.mute: MutedVolumeLevel.low: 0-25VolumeLevel.medium: 25-50VolumeLevel.high: 50-75VolumeLevel.peak: 75-100 |
Icon | Description | Download link |
![]() | Volume indicator icon; use for VolumeLevel.low or VolumeLevel.medium. | |
![]() | Mute icon; use for VolumeLevel.mute. |

Widget _buildCallCoreView() {Map<NetworkQuality, Image> networkQualityIcons = {NetworkQuality.bad : Image.asset(''), // Icon for poor network};return CallCoreView(controller: CallCoreController.create(),networkQualityIcons: networkQualityIcons,);}
Parameter | Type | Required | Description |
networkQualityIcons | Map | No | Maps network quality to icons. Keys: NetworkQuality.unknownNetworkQuality.excellentNetworkQuality.goodNetworkQuality.poorNetworkQuality.badNetworkQuality.veryBadNetworkQuality.down |
Icon | Description | Download link |
![]() | Poor network indicator; use for NetworkQuality.bad, NetworkQuality.veryBad, or NetworkQuality.down. |
Widget _buildCallCoreView() {Image defaultAvatarImage = Image.asset(''); // Default avatar imagereturn CallCoreView(controller: CallCoreController.create(),defaultAvatar: defaultAvatarImage,);}
Parameter | Type | Required | Description |
defaultAvatar | Image | No | Default avatar image |
Icon | Description | Download link |
![]() | Default avatar; use as a placeholder when loading fails or no avatar is set. |

Widget _buildCallCoreView() {Image loading = Image.asset(''); // Loading animation resourcereturn CallCoreView(controller: CallCoreController.create(),loadingAnimation: loading,);}
Parameter | Type | Required | Description |
loadingAnimation | Image | No | GIF animation resource for waiting state |
Icon | Description | Download link |
![]() | Waiting animation; use for group calls to indicate users in waiting state. |
CallStore.shared.state.activeCall for updates.activeCall.duration field in your widget. This value updates reactively—no timer needed.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,});@overrideWidget 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';}}}
UserProfile profile = UserProfile(userID: "", // Your UserIdavatarURL: "", // Avatar URLnickname: "", // Nickname to set);CompletionHandler result = await LoginStore.shared.setSelfInfo(userInfo: profile);if (result.errorCode == 0) {print("setSelfInfo success");} else {print("setSelfInfo failed");}
Parameter | Type | Required | Description |
userProfile | Yes | userID: User IDavatarURL: Avatar URLnickname: Nickname | |
completion | CompletionHandler | No | Callback returns result of set operation |
Float, group calls use Grid.Float Mode | Grid Mode | PIP Mode |
![]() | ![]() | ![]() |
Layout: Full screen self-view while waiting; after connecting, full screen remote view with self-view as a floating window. Interaction: Floating window supports drag and click-to-swap with the main view. | Layout: Grid layout for all participants, suitable for 2+ users. Supports click-to-enlarge. Interaction: Click any participant's view to enlarge. | Layout: 1v1 shows fixed remote view; multi-party uses active speaker strategy with full screen for the current speaker. Interaction: Waiting state shows self-view; after connecting, call duration is displayed. |
CallCoreController controller = CallCoreController.create();CallLayoutTemplate template = CallLayoutTemplate.float;controller.setLayoutTemplate(template);
Parameter | Type | Required | Description |
template | Yes | Layout mode: float: Waiting is self full screen, answered is remote full screen/self floating window.grid: All participants tiled.pip: 1v1 always remote, group uses Active Speaker. |
void startCall(List<String> userIdList, CallMediaType mediaType) {CallParams params = CallParams(timeout: 30, // Set call waiting timeout);CallStore.shared.calls(userIdList, mediaType, params);}
Parameter | Type | Required | Description |
userIdList | List | Yes | List of target userIds. |
mediaType | Yes | Specifies call type: audio or video. | |
params | No | Additional options: room ID, timeout, custom data, group ID, ephemeral flag. |
_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,),),),),),),),);}
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 = falseoverride 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") ?: falseval success = enablePIP(enable)result.success(success)}"enterPictureInPicture" -> {val success = enterPIP()result.success(success)}else -> result.notImplemented()}}}override fun onUserLeaveHint() {super.onUserLeaveHint()// Automatically enter PiP when user presses Homeif (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 = enablereturn true}return false}private fun enterPIP(): Boolean {if (!enablePictureInPicture) return falseif (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}}
<activityandroid: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-dataandroid: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>
import 'package:flutter/services.dart';class PipManager {static const MethodChannel _channel = MethodChannel('atomic_x/pip');/// Enable/disable PiP featurestatic Future<bool> enablePictureInPicture(bool enable) async {try {final result = await _channel.invokeMethod<bool>('enablePictureInPicture', {'enable': enable});return result ?? false;} catch (e) {return false;}}/// Enter PiP mode immediatelystatic Future<bool> enterPictureInPicture() async {try {final result = await _channel.invokeMethod<bool>('enterPictureInPicture');return result ?? false;} catch (e) {return false;}}}
Background Modes under Signing & Capabilities and check Audio, AirPlay, and Picture in Picture. 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"}]}}''');});
import 'package:tencent_rtc_sdk/trtc_cloud.dart';TRTCCloud.sharedInstance().then((trtcCloud) {trtcCloud.callExperimentalAPI('''{"api": "configPictureInPicture","params": {"enable": false}}''');});
CallStore.shared.state.selfInfo.addListener(() {CallParticipantInfo info = CallStore.shared.state.selfInfo.value;if (info.status == CallParticipantStatus.accept || info.status == CallParticipantStatus.none) {// Stop ringtonereturn;}if (info.status == CallParticipantStatus.waiting) {// Play ringtone}});
<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><serviceandroid:name=".CallForegroundService"android:enabled="true"android:exported="false"android:foregroundServiceType="camera|microphone" /></application></manifest>
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 = 1001private 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()// Start foreground notification to ensure background capture permissionstartForeground(NOTIFICATION_ID, createNotification())}override fun onBind(intent: Intent?): IBinder? = nullprivate fun createNotification(): Notification {return NotificationCompat.Builder(this, CHANNEL_ID).setContentTitle("In Call").setContentText("App is running in background to keep the call alive").setSmallIcon(android.R.drawable.ic_menu_call) // Replace with your app icon.setPriority(NotificationCompat.PRIORITY_HIGH).build()}private fun createNotificationChannel() {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {val channel = NotificationChannel(CHANNEL_ID,"Call Keep-Alive Service",NotificationManager.IMPORTANCE_HIGH)val manager = getSystemService(NotificationManager::class.java)manager.createNotificationChannel(channel)}}}
Target > Signing & Capabilities.+ Capability.Background Modes.Audio, AirPlay, and Picture in PictureVoice over IPRemote notifications (optional for offline push)Info.plist will include:<key>UIBackgroundModes</key><array><string>audio</string><string>voip</string><string>remote-notification</string></array>
viewDidLoad or before initiating a call:import AVFoundation/*** Configure audio session for background audio capture** Recommended scenarios:* 1. In call interface's viewDidLoad* 2. Before initiating a call (calls)* 3. Before answering a call (accept)*/private func start() {let audioSession = AVAudioSession.sharedInstance()do {// Set session category to play and record// .allowBluetooth: Support Bluetooth headset// .allowBluetoothA2DP: Support high-quality Bluetooth audio (A2DP)try audioSession.setCategory(.playAndRecord, options: [.allowBluetooth, .allowBluetoothA2DP])// Activate audio sessiontry audioSession.setActive(true)} catch {// Audio session configuration failed}}
start() via MethodChannel at the appropriate time to enable background keep-alive.Feature | Description | Integration Guide |
Answer the first call | Quick integration guide for handling incoming calls, including answer/reject controls and call interface invocation. |
symbol not found errors in your iOS Release build, it’s likely because Xcode’s symbol stripping removed TRTC C symbols used by tencent_rtc_sdk (via Flutter FFI). To fix:

フィードバック