ConnectyCube / connectycube-reactnative-samples

Chat and Video Chat code samples for React Native, ConnectyCube
https://connectycube.com
Apache License 2.0
124 stars 111 forks source link

Experience strange issues when try to answer / reject incoming call when leave app in background for a long time (more than 1 hour) #306

Closed kl-pen closed 1 year ago

kl-pen commented 1 year ago

We use Push notification (VOIP push for iOS) and Call Keep to handle incoming call in background state. However, we experience strange issues if the receiver side leaves app in background state for a long time (more than 1 hour)

Issue1: If leave the app in background state for a long time and try to answer incoming call within 5 secs, the call will never get established (due to receiver side will not get onCall() event)

  1. [User A] leaves app in background for more than 1 hour.
  2. [User B] calls [User A].
  3. [User A] receives push notification and incoming call screen is displayed (via Call keep)
  4. [User A] pressed answer call within 5 seconds.
  5. [User A] doesn’t receive onCall Event from ConnectyCube.videochat.onCallListener.
  6. [User B] doesn’t receive onAcceptCall event from ConnectyCube.videochat.onAcceptCallListener. App continues to show outgoing call screen until timeout is reach.

Issue2: If leave the app in background state for a long time and try to reject incoming call within 5 secs, the calling side doesn’t receive onReject event.

  1. [User A] leaves app in background for more than 1 hour.
  2. [User B] calls [User A].
  3. [User A] receives push notification and incoming call screen is displayed (via Call keep)
  4. [User A] pressed reject call within 5 seconds. The incoming call screen is closed.
  5. [User B] doesn’t receive onRejectCall event from ConnectyCube.videochat.onRejectCallListener. App continues to show outgoing call screen until timeout is reach.

Question What is the correct step to handle when receiving incoming call while app in background state for a long time? Should we re-login to connect cube every time when app change from background to active state?

Currently we do below connectycube login process when app is first open only.

ccvlad commented 1 year ago

@kl-pen

In both cases you should restore chat connection. Usually mobile OS prevents chat connect after 2-5 mins in background 1) check your chat connection state - ConnectyCube.chat.isConnected; 2) connect to chat if chat is not connected - if (ConnectyCube.chat.isConnected) { ConnectyCube.chat.connect() }.

FYI. The ConnectyCube's session also expires after two hour of inactivity. I suggest you to manage this. 1) You can store the ConnectyCube's session and compare session.updated_at + 2 * 60 * 60 (timestamp in seconds) with current time and define to use the stored session (ConnectyCube.setSession(storedSession)) or create new one (ConnectyCube.createSession()). 2) To handle that a session was expired see the session expiration docs

kl-pen commented 1 year ago

Thank so much for your reply.

Regarding your explanation for handling chat connection and session expired ,

1. Since our app use External authentication via Custom Identity Provider (CIdP) for user authentication, after creating a new session using ConnectyCube.createSession(), should we also do ConnectyCube.login()& ConnectyCube.chat.connect() as well ?

Please see our code for handling createSession & connect to chat.

//Create ConnectyCube Session & login to Custom Identity Provider
let data = await this.createSession();
if (!data) {
    this._inLogin = false;
    return false;
}
let obj = {
    userId: data.id,
    password: data.token,
}
//Login to ConnectyCube Chat
let _connect = await this.connect(obj)
//CreateSession Function
createSession = async () => {
        let session = await ConnectyCube.createSession().catch((error) => {
            return { errors: error };
        });

        if (session?.errors) {
            return false
        }

        let account = await Storage.getDataKeychain();
        if (!account?.AccountVOIPID || !account?.VOIPToken) {
            return false
        }
        const userCredentials = {
            AccountVOIPID: account.AccountVOIPID,
            VOIPToken: account.VOIPToken,
        };

       //LOGIN to Custom Identity Provider and use return data to connect to chat
        let users = await ConnectyCube.login(userCredentials).catch((error) => {
            return { errors: error };
        });

        if (users?.errors) {
            return false
        }

        let obj = Object.assign(session, users);
        await Storage.storeData('Session', obj);
        return obj;
    }
//Connect to Chat Function
connect = async (obj) => {
        if (!ConnectyCube?.chat?.connect) {
            return false
        }

        //Using authentication data return from createSession()  to connect to chat
        let _connect = await ConnectyCube.chat.connect(obj).catch((error) => {
            return { errors: error };
        });
        if (_connect?.errors) {
            return false
        }
        this._inLogin = false;

        return true
    }

2. Similar to first question, when session expired do we need to do ConnectyCube.login()& ConnectyCube.chat.connect() or just below?

  sessionExpired: (handleResponse, retry) => {
            console.log("-----------sessionExpired");

            ConnectyCube.createSession()
            .then(retry)
            .catch((error) => {});
   },

3. To handle incoming call when session has expired & device is in Lock Screen state, we also try using chat.ping() to recheck connection & recreate session again. Is this the correct way? or we should just check using only if (ConnectyCube.chat.isConnected) { ConnectyCube.chat.connect() }.

Due to sometimes we receive below error and we are not sure what is the meaning of below.

 ERROR  [Chat] xmppClient.start error [Error: Connection is not offline]
 LOG  -----connect error [Error: Connection is not offline]

4. Since we want to try handling session expired case correctly, is there any API we can use to simulate this without waiting for 2 hours? (ex. API to manually expired / destroy session)

Sorry for such a long question. We deeply appreciate for all you help and support and looking forward to hearing back from you soon.

ccvlad commented 1 year ago
  1. ConnectyCube.createSession() creates empty session (w/o user). The ConnectyCube.login(params) is needed to upgrade session on server and pass user auth data to the session. It's all you need to have a normal session.

  2. Yes. createSession + login > chat.connect

  3. The error is from XMPP chat lib and it really looks strange. Usually it shows when you are trying to use chat connection before chat is connected

  4. A sessions' expiration time is a server setting. You can use ConnectyCube.destroySession() to make behavior that similar to expired session.

I prefer to use custom chat statuses to manage the chat connection. E.g. this.chatStatus = 'disconnected' before the ConnectyCube.chat.connect() wouldn't call, this.chatStatus = 'connecting' with the same time with ConnectyCube.chat.connect(), this.chatStatus = 'connected' after successful connected and this.chatStatus = 'failed' otherwise. Setup ConnectyCube.chat.onDisconnectedListener = this.onDisconnect.bind(this); where onDisconnect = () => { this.chatStatus = 'disconnected'; }. Also pay attention to '@react-native-community/netinfo' library to know offline/online state.

FYI. Here the sample code to use AppState with mark chat as active/inactive:

  handleAppStateChange = (state) => {
    if (state === 'inactive') {
      // handle only 'active' and 'background'
      // https://facebook.github.io/react-native/docs/appstate
      return;
    }

    if (ConnectyCube.chat.isConnected) {
      if (state === 'active') {
        ConnectyCube.chat.markActive();
      } else {
        ConnectyCube.chat.markInactive();
      }
    } else if (isOnline) { // from NetInfo lib
      ConnectyCube.chat.connect(params); // connect with user params
    }
  };

  // AppState.addEventListener('change', handleAppStateChang);
kl-pen commented 1 year ago

Thank you so much for your detailed explanation!

As you suggested, we tried to handle session expired and chat re-connect by doing createSession + login > chat.connect and used custom chat statuses to manage chat connection.

However, we sometimes found below error message when we tried to do createSession + login > chat.connect in ConnectyCube.chat.onDisconnectedListener event. And the connection process failed.

 ERROR  [Chat] xmppClient.start error [Error: Connection is not offline]
 LOG  -----connect error [Error: Connection is not offline]

Question: 1. Since we make sure to do createSession + login > chat.connect after receive onDisconnect but still get this error, is there any other reasons for this [Error: Connection is not offline] message?

2. Currently we use below config which enable both ping & chat auto-reconnect. Should we leave this setting enable as it is? or should we disable them (due to manually handling createSession + login > chat.connect by ourselves)?

Connectycube Config

export const appConfig = {
    debug: { mode: 0 },
    chat: {
        reconnectionTimeInterval: 2,
        reconnect: {
            enable: true,
            timeInterval: 2
        },
        streamManagement: {
            enable: true
        },
        ping: {
            enable: true,
            timeInterval: 30
        }
    },
    videochat: {
        alwaysRelayCalls: false,
        answerTimeInterval: 45,
        dialingTimeInterval: 3, 
        disconnectTimeInterval: 35,
        statsReportTimeInterval: false,       
    },
ccvlad commented 1 year ago

Oh, sorry... I forgot about autoreconnect. I your case would be better to manually manage the connection to chat. Change your config:

export const appConfig = {
    debug: { mode: 0 },
    chat: {
      streamManagement: {
        enable: true,
      },
      reconnect: {
        enable: false,
      },
    },

Try to do it w/o chat.ping() and remove it from your config. Please, show full log if the error [Error: Connection is not offline] keeps repeating.

kl-pen commented 1 year ago

Thank you so much for your reply.

  1. So you mean in our case we shouldn't use auto-reconnect & auto-ping at all ? (because of using External Authentication via Custom identity provider?)

  2. If so, it means we need to manually handle reconnect by ourselves. So we need to manually checking connection & attempt reconnect in multiple events such as onDisconnect , AppState: Active, IncomingCall/OutgoingCall ??

  3. With auto-reconnect config, it seems after a fews reconnect attempts it stopped retry eventually. Is there any event/listener to detect if reconnect attempt is stopped or resulted in error ? Because if there is such event, we think we can trigger our own reconnect flow using createSession + login > chat.connect.

ccvlad commented 1 year ago

No, I recommend it, because of React Native. The reconnect: true works fine with web applications, but it brings some issues in React Native when OS close the WebSocket connection in background. It is normal to manage chat connections via statuses. Do not worry about ConnectyCube’s session, cause it works via HTTPS instead of chat (it keeps WS connection to be online)

kl-pen commented 1 year ago

Thank you so much for your support. We have tried as you suggested below.

Now after we answer the call, the connection will get established successfully. However after implementing the above, we seem to run into another problem....

Issue: After answer incoming call when leave the app in background for more than 2 hours, the call will get established but sometimes it will immediately got disconnect after 1-2 secs.

Questions 1. What is the correct way to use onSessionConnectionStateChangedListener to monitor VOIP connection state and update call UI accordingly ?

Now we handle as below.

2. Why after answer call the receiver side sometimes receive SessionConnectionState.CLOSED immediately after SessionConnectionState.CONNECTED (1-2 secs)? Should we ignore CLOSED state ?


For more information on device & issue log please see below.

Device: Samsung A21, iPhone 11/14 P.S. This problem seems to happened more frequent if the receiver side is iPhone.

Version: react-native@0.67.4, react-native-connectycube@3.22.1

Log of Receiver (UserA) :

LOG [14:36:14 GMT+0700] _onDisconnectedListener: received disconnect event
LOG [14:36:18 GMT+0700] _reConnectWithSession: manual check session & handle reconnect 
LOG [14:36:21 GMT+0700] _connectionState 1: CONNECTING
LOG [14:36:24 GMT+0700] _connectionState 2: CONNECTED
LOG [14:36:25 GMT+0700] _connectionState 5: CLOSED

Log of Caller (UserB):

LOG [14:34:45 GMT+0700] finish login flow: createSession + login + chatconnect()
LOG [14:36:26 GMT+0700] _connectionState 1: CONNECTING
LOG [14:36:26 GMT+0700] _connectionState 2: CONNECTED
LOG [14:36:28 GMT+0700] _connectionState 5: CLOSED
ccvlad commented 1 year ago

Hi, @kl-pen !

It is normal behavior for mobile's OS. Need to configure background service to solve the issue.

Pay attention to this guide.

The onSessionConnectionStateChangedListener just informs about call peer state. I think that you use it correct when update some info on UI depending on current state.

kl-pen commented 1 year ago

Thank you so much for your response.

We have already followed your guide and finish config background mode correctly. However the issue still occurred, which we think it is not normal behavior (call end abruptly after pressed accept call without user doing anything)

We tested that our app works correctly in background mode in normal case, but seems to have issue only when we leaves the app in background for a long time. (our voip app uses voice call only and doesn't use video call)

Upon further analyzing our log, the issue seems to be on the Caller side as below.

Why do we received ConnectyCube event onStopCallListener even though users didn't do anything?

Please see our debug log taken from Caller side in the attached file below. ConnectyCubeLog.pdf

ccvlad commented 1 year ago

Hi, @kl-pen

Is caller receive event in the onStopCallListener immediately after callee answers or after some interval (reject or no answer)?

Do you have logs from callee side?

kl-pen commented 1 year ago

Thank you so much for your reply and sorry for taking such a long time to response back.

Upon analyzing logs of both caller & callee sides very carefully, it seems that the root cause is because our app mishandled case when user accepts call at the time session's already expired (which caused the callee side to force end call and triggered onStopCallListener event on the caller side).

After we fixed the above issue, now the call doesn't end abruptly anymore (even after leaving the app for long time in background). We are deeply sorry for the misunderstanding on our part.

Again, thank you so much for all the guidance and support. Now our app can do VOIP call in background and it works correctly!!

ccvlad commented 1 year ago

No problem! Good to know, that you have managed to solve the issue