AgoraIO-Extensions / Agora-Flutter-SDK

Flutter plugin of Agora RTC SDK for Android/iOS/macOS/Windows
https://pub.dev/packages/agora_rtc_engine
MIT License
746 stars 386 forks source link

Fatal Exception: java.lang.NullPointerException #2023

Open dpatel-jac opened 1 month ago

dpatel-jac commented 1 month ago

Bug:- Android App Getting Crashed after accepting Incoming call on system apps

We integrate agora as video calling feature in our app. It working fine in most of the cases, but in situation application will terminate due to NullPointerException. Please follow exception logs and suggest me possible outputs.

Exception details Fatal Exception: java.lang.NullPointerException Attempt to invoke interface method 'void io.agora.rtc2.internal.AudioRoutingController$ControllerState.setState(int)' on a null object reference

StackTrace io.agora.rtc2.internal.AudioRoutingController$2.run (Unknown Source:7) android.os.Handler.handleCallback (Handler.java:883) android.os.Handler.dispatchMessage (Handler.java:100) android.os.Looper.loop (Looper.java:214) android.os.HandlerThread.run (HandlerThread.java:67)

Here is sample code for connection to agora channel.

class MeetHelperNotifier with ChangeNotifier {

  final AppSettings setting;
  final CallArguments args;
  late RtcEngine _engine;
  RtcEngine get engine => _engine;

  MeetHelperNotifier(this.setting, this.args) {
    _engine = createAgoraRtcEngine();
  }

  bool isSessionEnded = false;
  bool isRecordingRelatedError = false;
  ValueNotifier<bool> isInSession = ValueNotifier<bool>(false);
  ValueNotifier<bool> isCallConnected = ValueNotifier<bool>(false);

  int? currentUserId;
  Map<int, VideoStatus> userVideoStatus = <int, VideoStatus>{};
  Map<int, bool> userAudioStatus = <int, bool>{};
  ValueNotifier<List<int>> users = ValueNotifier<List<int>>([]);
  List<int> get otherUsers => users.value.where((e) => e != currentUserId).toList();

  reloadUserList() {
    users.value = List<int>.from(users.value);
  }

  void _connect(String token) async {
    try {

      await _engine.initialize(RtcEngineContext(
        appId: configs['Agora_APPID'],
        channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
        logConfig: const LogConfig(level: LogLevel.logLevelNone),
      ));

      await _engine.setAudioProfile(
        profile: AudioProfileType.audioProfileDefault,
        scenario: AudioScenarioType.audioScenarioMeeting,
      );

      /// setup noise cancellation mode
      _engine.setAINSMode(enabled: true, mode: AudioAinsMode.ainsModeAggressive);

      await _engine.setVideoEncoderConfiguration(
        const VideoEncoderConfiguration(
          frameRate: 15,
          bitrate: 400,
          dimensions: VideoDimensions(width: 640, height: 360),
          orientationMode: OrientationMode.orientationModeAdaptive,
          degradationPreference: DegradationPreference.maintainBalanced,
        ),
      );

      _engine.registerEventHandler(
          RtcEngineEventHandler(
            onError: (ErrorCodeType err, String msg) {
              /// Check only when session not connected
              if (!isInSession.value) {
                /// Permission related error no need to terminate call
                if ([ErrorCodeType.errAdmInitRecording, ErrorCodeType.errAdmStartRecording, ErrorCodeType.errAdmStartPlayout].contains(err)) {
                  isRecordingRelatedError = true;
                  return;
                }
                isInSession.value = false;
                users.value = <int>[];
                isSessionEnded = true;
                Fluttertoast.showToast(
                  msg: 'Unable to join this call at this time.\n Detail: ${err.name}',
                  toastLength: Toast.LENGTH_LONG,
                );
                isInSession.notifyListeners();
              }
            },
            onJoinChannelSuccess: (RtcConnection connection, int elapsed) async {
              debugPrint('local user ${connection.localUid} joined');
              currentUserId = 0;
              isInSession.value = true;
              users.value = List.from([currentUserId!]);

              /// Update callkit mute and speaker status
              if (Platform.isIOS) {
                //_updateCallKitMuteStatus();
                await _engine.adjustAudioMixingPublishVolume(100);
                await _engine.enableAudioVolumeIndication(interval: 10000, smooth: 3, reportVad: false);
                await _engine.adjustRecordingSignalVolume(100);
                await _engine.adjustPlaybackSignalVolume(100);
                await _engine.adjustAudioMixingVolume(100);
                await _engine.setDefaultAudioRouteToSpeakerphone(true);
              }
            },
            onLeaveChannel: (RtcConnection connection, RtcStats stats) {
              isInSession.value = false;
              users.value = <int>[];
              isSessionEnded = true;
            },
            onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {
              final userList = List<int>.from(users.value);
              userList.add(remoteUid);
              users.value = List<int>.from(userList.toSet());
              if (userList.length > 1) {
                isCallConnected.value = true;
              }
              userVideoStatus.putIfAbsent(remoteUid, () => VideoStatus.on);
            },
            onUserOffline: (RtcConnection connection, int remoteUid, UserOfflineReasonType reason) {
              final userList = List<int>.from(users.value);
              userList.removeWhere((e) => e == remoteUid);
              users.value = List<int>.from(userList.toSet());
            },
            onRemoteVideoStateChanged: (RtcConnection connection, int remoteUid, RemoteVideoState state, RemoteVideoStateReason reason, int elapsed) {
              logger.v('remoteUid :: $remoteUid reason :: $reason');
              if (reason == RemoteVideoStateReason.remoteVideoStateReasonSdkInBackground) {
                userVideoStatus[remoteUid] = VideoStatus.pause;
                reloadUserList();
                return;
              }
              bool isVideoOn = ![RemoteVideoStateReason.remoteVideoStateReasonRemoteMuted, RemoteVideoStateReason.remoteVideoStateReasonRemoteOffline].any((e) => e == reason);
              final statusInDict = userVideoStatus[remoteUid] == VideoStatus.on;
              if (statusInDict != isVideoOn) {
                userVideoStatus[remoteUid] = isVideoOn ? VideoStatus.on : VideoStatus.off;
                reloadUserList();
              }
              logger.v('remoteVUid :: $remoteUid reason :: $reason elapsed :: $elapsed time: ${DateTime.now()}');
            },
            onRemoteAudioStateChanged: (RtcConnection connection, int remoteUid, RemoteAudioState state, RemoteAudioStateReason reason, int elapsed) {
              logger.v('remoteAUid :: $remoteUid reason :: $reason elapsed :: $elapsed time: ${DateTime.now()}');
            }
          )
      );

      await _engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
      await _engine.enableVideo();

      await _engine.enableAudio();

      await _engine.startPreview();

      await _engine.joinChannel(
        token: token,
        channelId: args.sessionName,
        uid: setting.deviceId?.convertUserUIdToInt() ?? 0,
        options: const ChannelMediaOptions(),
      );

    } catch (error) {
      logger.e('Error : ${error.runtimeType}');
      Fluttertoast.showToast(
        msg: 'Error :: ${e.toString()}',
        toastLength: Toast.LENGTH_LONG,
      );
    }
  }

  Future<void> disconnect() async {
    _engine.leaveChannel().then((value) {

      _engine.release();
    });
  }
}
littleGnAl commented 1 month ago

Thanks for reporting, I found this is a known issue internally, it will be fixed in the next release.

dpatel-jac commented 1 month ago

@littleGnAl , Is there any ETA for new version for this issues? or let me know if you provide intermediate solution to mitigate this crash issues.

littleGnAl commented 1 month ago

It's, unfortunately, there's no workaround in the agora_rtc_engine since it crashed in the Agora Native SDK.