startCall method
Start a Call (Single 1-to-1 or Group)
Implementation
Future<bool> startCall({
required CallType callType,
required CallMode callMode,
required String currentUserId,
String? currentUserName,
String? currentUserAvatar,
String? remoteUserId,
String? remoteUserName,
String? remoteUserAvatar,
String? groupId,
String? customChannelName,
int? customUid,
}) async {
try {
_stopSessionTimer();
_cancelRingTimeoutTimer();
_cancelGracePeriodTimer();
_callType = callType;
_callMode = callMode;
_remoteUserId = remoteUserId;
_remoteUserName = remoteUserName;
_remoteUserAvatar = remoteUserAvatar;
_errorMessage = null;
_isCameraEnabled = (callType == CallType.video);
_isMicEnabled = true;
_isSpeakerEnabled = true;
_channelName = customChannelName ??
(callMode == CallMode.single
? _channelService.generateSingleChannel(
callerId: currentUserId,
receiverId: remoteUserId ?? 'remote_user',
)
: _channelService.generateGroupChannel(
groupId: groupId ?? 'default_group',
));
_localUid = customUid ?? (currentUserId.hashCode.abs() % 100000 + 1);
// Create Real-Time Call Session
_currentSession = CallSession(
callId: 'session_${DateTime.now().millisecondsSinceEpoch}',
channelName: _channelName!,
callType: callType,
callMode: callMode,
);
_participants.clear();
// Register local participant
_participants[_localUid] = CallParticipant(
uid: _localUid,
userId: currentUserId,
name: currentUserName ?? 'You',
avatarUrl: currentUserAvatar,
isLocal: true,
isMuted: false,
isVideoEnabled: _isCameraEnabled,
isSpeaking: false,
isConnected: true,
joinedAt: DateTime.now(),
);
// Generate dynamic AccessToken2
_setState(CallState.requestingToken);
final token = _tokenService.generateToken(
channelName: _channelName!,
uid: _localUid,
);
// Validate App ID before initializing engine
if (_tokenService.appId.isEmpty || _tokenService.appId == 'YOUR_AGORA_APP_ID') {
throw Exception('Invalid Agora App ID. Please initialize AgoraCallController with a valid 32-character App ID from console.agora.io');
}
// Initialize Engine
_engine = createAgoraRtcEngine();
await _engine!.initialize(RtcEngineContext(
appId: _tokenService.appId,
channelProfile: ChannelProfileType.channelProfileCommunication,
));
_registerEventHandlers();
if (callType == CallType.video) {
await _engine!.enableVideo();
await _engine!.enableAudio();
if (!kIsWeb) {
// startPreview is not supported on web platform
try { await _engine!.startPreview(); } catch (e) { debugPrint('startPreview skipped: $e'); }
}
} else {
await _engine!.enableAudio();
await _engine!.disableVideo();
}
// Configure initial volume indication safely
try {
await _engine!.enableAudioVolumeIndication(
interval: 200,
smooth: 3,
reportVad: true,
);
} catch (e) {
debugPrint('Pre-join audio setup warning (non-fatal): $e');
}
// Join Channel with explicit media publish flags (required on Web)
_setState(CallState.joining);
debugPrint('🔗 joinChannel | channel: $_channelName | uid: $_localUid | token: ${token.substring(0, token.length > 20 ? 20 : token.length)}...');
await _engine!.joinChannel(
token: token,
channelId: _channelName!,
uid: _localUid,
options: ChannelMediaOptions(
clientRoleType: ClientRoleType.clientRoleBroadcaster,
channelProfile: ChannelProfileType.channelProfileCommunication,
publishMicrophoneTrack: _isMicEnabled,
publishCameraTrack: _callType == CallType.video,
autoSubscribeAudio: true,
autoSubscribeVideo: true,
),
);
// Post-join speakerphone configuration — not applicable on web
if (!kIsWeb) {
try {
await _engine!.setEnableSpeakerphone(_isSpeakerEnabled);
} catch (e) {
debugPrint('Post-join speakerphone setup error: $e');
}
}
// Stay in Calling... state & start 60s ring timeout until remote user joins
_startRingTimeoutTimer();
return true;
} catch (e) {
_errorMessage = e.toString();
_setState(CallState.error);
onErrorCallback?.call(_errorMessage!);
return false;
}
}