1-to-1 audio call
This page provides a code example for implementing a 1-to-1 audio call.
Prerequisites
Before you begin, you must do the following:
- Initialize PlanetKit.
- Get a proper access token.
- Check the overall API usage flow in 1-to-1 call flow.
Considerations for implementing a 1-to-1 audio call
To receive call notifications on the callee side, you must implement a notification system or integrate an external push notification system such as Apple Push Notification service (APNs) or Firebase Cloud Messaging (FCM).
You also need to know what information must be delivered to the callee. Refer to Role of the app server, which describes cc_param, the data that must be delivered by your application.
After calling makeCall() or verifyCall(), you must check the reason property of the returned PlanetKitMakeCallResult or PlanetKitVerifyCallResult.
- If the
reasonisPlanetKitStartFailReason.none, it means success. - Otherwise, it means failure and you must take an appropriate action based on the
reason.
Request device permissions
Use Dart permission_handler package to request phone and microphone permission.
import 'package:permission_handler/permission_handler.dart';
final status = await [Permission.microphone, Permission.phone, Permission.bluetoothConnect].request();
Prepare variables
Prepare variables for key properties and event handlers.
PlanetKitCallEventHandleris used to handle status change events of a 1-to-1 call. For more information, see 1-to-1 call flow.PlanetKitMyMediaStatusHandleris used to handle the local user's media status change events. You can update the local user's UI based on these events, such as when the mic becomes muted or unmuted, or when the audio description is updated.
final String _myUserId = "test user id";
final String _serviceId = "test service id";
PlanetKitCall? _call;
final _eventHandler = PlanetKitCallEventHandler(
onWaitConnected: (call) => print("wait connected"),
onVerified: (call, peerUseResponderPreparation) => print("verified"),
onConnected: (call, isInResponderPreparation, shouldFinishPreparation) => print("connected"),
onDisconnected: (call, reason, source, byRemote) => print("disconnected $reason"));
final _myMediaStatusHandler = PlanetKitMyMediaStatusHandler(
onMicMute: (status) => print("mic muted"),
onMicUnmute: (status) => print("mic unmuted"),
onAudioDescriptionUpdate: (status, averageVolumeLevel) => print("audio description updated"));
Make an outgoing call (caller side)
To make a call, call PlanetKitManager.instance.makeCall() with a PlanetKitMakeCallParam that you can create using PlanetKitMakeCallParamBuilder().
Future<bool> makeCall(String peerId, String accessToken) async {
final builder = PlanetKitMakeCallParamBuilder()
.setMyUserId(_myUserId)
.setMyServiceId(_serviceId)
.setPeerUserId(peerId)
.setPeerServiceId(_serviceId)
.setAccessToken(accessToken);
PlanetKitMakeCallParam? param;
try {
param = builder.build();
} catch (error) {
print("failed to build make call param $error");
return false;
}
final result = await PlanetKitManager.instance.makeCall(param, _eventHandler);
if (result.reason != PlanetKitStartFailReason.none) {
print("make call failed. reason: $result.reason");
return false;
}
// Store call instance and set MyMediaStatusHandler.
_call = result.call;
_call?.myMediaStatus.setHandler(_myMediaStatusHandler);
return true;
}
Respond to an incoming call (callee side)
When you receive a push message from the app server, you must parse the cc_param from the message and create a ccParam. This data is a required argument that must be passed when responding to a call.
To respond to a call, call PlanetKitManager.instance.verifyCall() with a proper PlanetKitVerifyCallParam. To build a PlanetKitVerifyCallParam, use PlanetKitVerifyCallParamBuilder().
Future<bool> verifyCall(String ccParamString) async {
final ccParam = await PlanetKitCcParam.createCcParam(ccParamString);
if (ccParam == null) {
print("failed to create ccparam string $ccParamString");
return false;
}
final builder = PlanetKitVerifyCallParamBuilder()
.setMyUserId(_myUserId)
.setMyServiceId(_serviceId)
.setCcParam(ccParam);
PlanetKitVerifyCallParam? param;
try {
param = builder.build();
} catch (error) {
print("failed to build verify call param $error");
return false;
}
final result =
await PlanetKitManager.instance.verifyCall(param, _eventHandler);
if (result.reason != PlanetKitStartFailReason.none) {
print("make call result $result.reason");
return false;
}
// Store call instance and set MyMediaStatusHandler.
_call = result.call;
_call?.myMediaStatus.setHandler(_myMediaStatusHandler);
return true;
}
Handling push notifications on a background isolate
When handling push notifications on a background isolate (such as FCM background messages on Android), keep in mind that each Flutter isolate has its own heap and event loop, preventing direct sharing of PlanetKitBackgroundCall instances. Instead, use PlanetKitManager.verifyBackgroundCall() on the background isolate to create a PlanetKitBackgroundCall, and pass the resulting backgroundCallId to the main isolate through a cross-isolate channel such as a SendPort/ReceivePort pair. On the main isolate, call PlanetKitManager.adoptBackgroundCall() with the backgroundCallId to manage the call.
On Android, you can support this flow with a native foreground service to maintain the process and handle long-running call logic. The foreground service can communicate with Flutter through platform channels, while the Dart side uses cross-isolate messaging to transfer lightweight data, such as backgroundCallId, between the background and main isolates.
// Background isolate
Future<void> verifyBackgroundCall(PlanetKitVerifyCallParam param, PlanetKitBackgroundCallEventHandler eventHandler) async {
final result = await PlanetKitManager.instance.verifyBackgroundCall(param, eventHandler);
if (result.reason != PlanetKitStartFailReason.none) {
print("Verify background call failed. ${result.reason}");
return;
}
YourForegroundServicePlugin.startService(
result.call!.backgroundCallId,
notificationTitle: 'Call in Progress');
}
// Adopt background call on main isolate
Future<void> adoptBackgroundCall(PlanetKitCallEventHandler eventHandler) async {
final backgroundCallId = YourForegroundServicePlugin.getCurrentServiceId();
final call = await PlanetKitManager.instance.adoptBackgroundCall(backgroundCallId, eventHandler);
if (call == null) {
print("adopt background call failed.");
return;
}
}
Accept an incoming call (callee side)
In general, the recipient needs time to decide whether to accept a new call after responding to the call. To accept a call, call PlanetKitCall.acceptCall().
The _call variable is a PlanetKitCall instance that has been verified by verifyCall().
Future<void> acceptCall() async {
return await _call?.acceptCall() ?? false;
}
End a call
To end a call, call endCall().
Future<bool> endCall() async {
return await _call?.endCall() ?? false;
}
Additional implementation for CallKit integration (Optional)
This step applies to iOS applications only.
When using the CallKit framework, you must set callKitType to PlanetKitCallKitType.user in PlanetKitMakeCallParam and PlanetKitVerifyCallParam.
Then, you must call PlanetKitCall.notifyCallKitAudioActivation() on CXProviderDelegate.provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) to ensure audio activation in PlanetKit.