So… I asked about this in the Jitsi Community Call a few weeks ago. We have a use case like this (it’s an imaginative use case, but bear with me):
- It is an app targeted at elderly people, who (according to the defined Persona) are not tech savvy.
- There is a feature in the app that allows the user to press a button in the app and gets a voice call established with someone to assist them.
- The user should be able to continue with what they are doing (without having to switch to a dedicated call UI)
- The call needs to continue if the app goes into the background (e.g. when the user presses home button)
After a bit of experiment, I got something to work, although a bit hacky.
First, I noted that:
- The Jitsi Meet View (or React Native View) does not need to be attached to an activity in order to function (i.e.
setContentViewis not neccessary). In fact, as soon as the
JitsiMeetViewinstance is created, the JS code starts executing.
- However, React Native does need an
Activitycontext to initialize (which is not a problem, since when we initiate the call, the user will always be on some activity in the app). The lifecycle methods (
onHostPauseetc.) also needs an active activity context, and will crash if they are called without a valid one, which a bit trickier. In other words, both the creation and destruction of
JitsiMeetViewneed to happen when there is an active
- The only reason why Jitsi Meet leaves the conference when the app goes back into the background is because
onHostPauseis called. If it doesn’t get called, React Native will continues to run, as long as the Android application process doesn’t get killed. The official Jitsi Meet Android app solves this by using Picture-in-Picture mode to keep the app in the foreground, but this is not the only way.
- Additionally, simply creating the
JitsiMeetViewinstance (with URL set to
"") will occupy camera and microphone resource. Because our app has other functionalities that need access to camera and microphone,
JitsiMeetViewinstance should only be created when there is a need for a call, and should be destroyed after the call.
- I created a class,
JitsiMeetHolder, a singleton initialized on Application start, that creates and holds the only Jitsi Meet View instance, and serves as an interceptor of lifecycle events.
Activity's pass their onStart/onStop lifecycle events to
JitsiMeetHolderwill suppress the
onPauseevent if there is an ongoing call. It will also note that the activity is “dead”, so any further requests (from the business logic of the container app) to start/stop a Jitsi Meet call will be “queued” until a new
- When any
Activityof the app comes into the foreground.
JitsiMeetHolderchecks for any pending requests, and then create/destroy the
JitsiMeetViewinstance using the new
- Whenever there is an ongoing Jitsi Meet call, I will launch a
Application.startForegroundService()), which does not perform any real tasks, just to inform the Android system not to kill our app.
The (partial) code is available at https://gist.github.com/ztl8702/7e1d2ec455727ef8d4127dc8ef53ccbf
I am sure if I look deeper into the React Native source code, I might be able to come up with a more elegant solution. But this approach that I described above has worked pretty well for us.
Update: Here is a screen recording demonstrating this “headless Jitsi call” feature in action:
The initiation (accepting incoming calls) is handle by our application logic. Our application server generates a unique URL for each call.
The initiator of the call (emulator on the left) joins the Jitsi Meet conference as it is waiting for the other to accept the call; the other client receives the Jitsi conference URL from the application server, and joins it after accepting the call.
The browser on the left is in the same Jitsi conference. You can see the two clients joining and leaving the conference.