startCall method

Future<bool> startCall({
  1. required CallType callType,
  2. required CallMode callMode,
  3. required String currentUserId,
  4. String? currentUserName,
  5. String? currentUserAvatar,
  6. String? remoteUserId,
  7. String? remoteUserName,
  8. String? remoteUserAvatar,
  9. String? groupId,
  10. String? customChannelName,
  11. int? customUid,
})

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;
  }
}