@cloudflare/realtimekit-react-native 1.2.0-staging.0 → 1.2.0-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/AndroidManifest.xml +6 -1
- package/android/src/main/java/com/cloudflare/realtimekit/KeepAliveService.java +176 -0
- package/android/src/main/java/com/cloudflare/realtimekit/RTKHelperModule.java +27 -60
- package/lib/commonjs/LocalMediaHandler.js +114 -82
- package/lib/commonjs/LocalMediaHandler.js.map +1 -1
- package/lib/commonjs/LocalMediaUtils.js +3 -5
- package/lib/commonjs/LocalMediaUtils.js.map +1 -1
- package/lib/commonjs/ReactHooks.js +27 -9
- package/lib/commonjs/ReactHooks.js.map +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/utils/version.js +1 -1
- package/lib/commonjs/utils/version.js.map +1 -1
- package/lib/module/LocalMediaHandler.js +114 -82
- package/lib/module/LocalMediaHandler.js.map +1 -1
- package/lib/module/LocalMediaUtils.js +3 -5
- package/lib/module/LocalMediaUtils.js.map +1 -1
- package/lib/module/ReactHooks.js +27 -8
- package/lib/module/ReactHooks.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/version.js +1 -1
- package/lib/module/utils/version.js.map +1 -1
- package/lib/typescript/LocalMediaHandler.d.ts +18 -4
- package/lib/typescript/ReactHooks.d.ts +18 -0
- package/lib/typescript/index.d.ts +1 -0
- package/lib/typescript/utils/version.d.ts +1 -1
- package/package.json +1 -1
- package/plugin/build/withRTK.js +50 -22
- package/plugin/src/withRTK.ts +58 -23
- package/android/src/main/java/com/cloudflare/realtimekit/ForegroundService.java +0 -46
- package/android/src/main/java/com/cloudflare/realtimekit/NotificationHelper.java +0 -126
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
|
17
17
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
|
|
18
18
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
|
19
|
-
<uses-permission android:name="android.permission.
|
|
19
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
|
20
20
|
<uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT" />
|
|
21
21
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
|
22
22
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
|
@@ -27,5 +27,10 @@
|
|
|
27
27
|
android:authorities="@string/blob_provider_authority"
|
|
28
28
|
android:exported="false"
|
|
29
29
|
/>
|
|
30
|
+
<service
|
|
31
|
+
android:name="com.cloudflare.realtimekit.KeepAliveService"
|
|
32
|
+
android:enabled="true"
|
|
33
|
+
android:exported="false"
|
|
34
|
+
android:foregroundServiceType="camera|microphone|mediaPlayback" />
|
|
30
35
|
</application>
|
|
31
36
|
</manifest>
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
package com.cloudflare.realtimekit;
|
|
2
|
+
|
|
3
|
+
import android.app.Notification;
|
|
4
|
+
import android.app.NotificationChannel;
|
|
5
|
+
import android.app.NotificationManager;
|
|
6
|
+
import android.app.PendingIntent;
|
|
7
|
+
import android.app.Service;
|
|
8
|
+
import android.content.Context;
|
|
9
|
+
import android.content.Intent;
|
|
10
|
+
import android.content.pm.ApplicationInfo;
|
|
11
|
+
import android.content.pm.PackageManager;
|
|
12
|
+
import android.content.pm.ServiceInfo;
|
|
13
|
+
import android.graphics.Bitmap;
|
|
14
|
+
import android.graphics.drawable.BitmapDrawable;
|
|
15
|
+
import android.graphics.drawable.Drawable;
|
|
16
|
+
import android.os.Build;
|
|
17
|
+
import android.os.IBinder;
|
|
18
|
+
import android.util.Log;
|
|
19
|
+
|
|
20
|
+
import androidx.core.app.NotificationCompat;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Foreground service that keeps the app alive in the background during a
|
|
24
|
+
* meeting. Declares foreground service types CAMERA, MICROPHONE, and
|
|
25
|
+
* MEDIA_PLAYBACK so Android does not kill the process while participants have
|
|
26
|
+
* camera / microphone active or audio is playing.
|
|
27
|
+
*
|
|
28
|
+
* Lifecycle is managed from JS via RTKHelper.startMeetingService() and
|
|
29
|
+
* RTKHelper.stopMeetingService(), which are called by LocalMediaHandler.
|
|
30
|
+
*/
|
|
31
|
+
public class KeepAliveService extends Service {
|
|
32
|
+
|
|
33
|
+
private static final String TAG = "KeepAliveService";
|
|
34
|
+
private static final int NOTIFICATION_ID = 42002;
|
|
35
|
+
|
|
36
|
+
static final String ACTION_START =
|
|
37
|
+
"com.cloudflare.realtimekit.KEEP_ALIVE_START";
|
|
38
|
+
|
|
39
|
+
// Notification config — set by RTKHelperModule.startMeetingService() before launch().
|
|
40
|
+
static volatile String sTitle = "In a call";
|
|
41
|
+
static volatile String sText = "Tap to return to your meeting";
|
|
42
|
+
static volatile String sChannelId = "rtk_meeting";
|
|
43
|
+
static volatile String sChannelName = "Ongoing Meeting";
|
|
44
|
+
|
|
45
|
+
// -----------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
public static void launch(Context context,
|
|
48
|
+
String title,
|
|
49
|
+
String text,
|
|
50
|
+
String channelId,
|
|
51
|
+
String channelName) {
|
|
52
|
+
sTitle = title != null ? title : "In a call";
|
|
53
|
+
sText = text != null ? text : "Tap to return to your meeting";
|
|
54
|
+
sChannelId = channelId != null ? channelId : "rtk_meeting";
|
|
55
|
+
sChannelName = channelName != null ? channelName : "Ongoing Meeting";
|
|
56
|
+
|
|
57
|
+
Intent intent = new Intent(context, KeepAliveService.class);
|
|
58
|
+
intent.setAction(ACTION_START);
|
|
59
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
60
|
+
context.startForegroundService(intent);
|
|
61
|
+
} else {
|
|
62
|
+
context.startService(intent);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public static void stop(Context context) {
|
|
67
|
+
context.stopService(new Intent(context, KeepAliveService.class));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// -----------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
@Override
|
|
73
|
+
public IBinder onBind(Intent intent) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@Override
|
|
78
|
+
public void onTaskRemoved(Intent rootIntent) {
|
|
79
|
+
// Stop when the user swipes the app away from recents.
|
|
80
|
+
stopSelf();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@Override
|
|
84
|
+
public int onStartCommand(Intent intent, int flags, int startId) {
|
|
85
|
+
if (intent == null) {
|
|
86
|
+
return START_NOT_STICKY;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
String action = intent.getAction();
|
|
90
|
+
|
|
91
|
+
if (ACTION_START.equals(action)) {
|
|
92
|
+
createNotificationChannel();
|
|
93
|
+
Notification notification = buildNotification();
|
|
94
|
+
try {
|
|
95
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
96
|
+
startForeground(NOTIFICATION_ID, notification,
|
|
97
|
+
ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
|
|
98
|
+
| ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
|
99
|
+
| ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
|
|
100
|
+
} else {
|
|
101
|
+
startForeground(NOTIFICATION_ID, notification);
|
|
102
|
+
}
|
|
103
|
+
} catch (RuntimeException e) {
|
|
104
|
+
Log.e(TAG, "startForeground failed, stopping service: " + e.getMessage());
|
|
105
|
+
stopSelf();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return START_NOT_STICKY;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// -----------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
private void createNotificationChannel() {
|
|
115
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
116
|
+
NotificationChannel channel = new NotificationChannel(
|
|
117
|
+
sChannelId, sChannelName, NotificationManager.IMPORTANCE_DEFAULT);
|
|
118
|
+
channel.enableVibration(false);
|
|
119
|
+
NotificationManager nm =
|
|
120
|
+
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
|
121
|
+
if (nm != null) {
|
|
122
|
+
nm.createNotificationChannel(channel);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private Notification buildNotification() {
|
|
128
|
+
Intent launchIntent = getPackageManager().getLaunchIntentForPackage(getPackageName());
|
|
129
|
+
if (launchIntent == null) {
|
|
130
|
+
launchIntent = new Intent();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
PendingIntent pendingIntent;
|
|
134
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
135
|
+
pendingIntent = PendingIntent.getActivity(
|
|
136
|
+
this, 0, launchIntent, PendingIntent.FLAG_IMMUTABLE);
|
|
137
|
+
} else {
|
|
138
|
+
pendingIntent = PendingIntent.getActivity(
|
|
139
|
+
this, 0, launchIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
int appIconResId = android.R.mipmap.sym_def_app_icon;
|
|
143
|
+
Bitmap largeIcon = null;
|
|
144
|
+
try {
|
|
145
|
+
PackageManager pm = getPackageManager();
|
|
146
|
+
ApplicationInfo ai = pm.getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
|
|
147
|
+
appIconResId = ai.icon;
|
|
148
|
+
Drawable drawable = ai.loadIcon(pm);
|
|
149
|
+
if (drawable instanceof BitmapDrawable) {
|
|
150
|
+
largeIcon = ((BitmapDrawable) drawable).getBitmap();
|
|
151
|
+
}
|
|
152
|
+
} catch (Exception e) {
|
|
153
|
+
Log.w(TAG, "Failed to load app icon: " + e.getMessage());
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, sChannelId)
|
|
157
|
+
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
158
|
+
.setContentTitle(sTitle)
|
|
159
|
+
.setContentText(sText)
|
|
160
|
+
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
|
161
|
+
.setContentIntent(pendingIntent)
|
|
162
|
+
.setOngoing(true)
|
|
163
|
+
.setAutoCancel(false)
|
|
164
|
+
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
165
|
+
.setOnlyAlertOnce(true)
|
|
166
|
+
.setUsesChronometer(false)
|
|
167
|
+
.setSmallIcon(appIconResId)
|
|
168
|
+
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE);
|
|
169
|
+
|
|
170
|
+
if (largeIcon != null) {
|
|
171
|
+
builder.setLargeIcon(largeIcon);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return builder.build();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
package com.cloudflare.realtimekit;
|
|
2
2
|
|
|
3
3
|
import android.app.Activity;
|
|
4
|
-
import android.app.ActivityManager;
|
|
5
|
-
import android.content.ComponentName;
|
|
6
|
-
import android.content.Intent;
|
|
7
4
|
import android.content.Context;
|
|
8
5
|
import android.content.pm.ActivityInfo;
|
|
9
6
|
import android.os.Build;
|
|
10
7
|
import android.os.Handler;
|
|
11
8
|
import android.os.PowerManager;
|
|
9
|
+
import android.util.Log;
|
|
12
10
|
import android.view.WindowManager;
|
|
13
11
|
|
|
14
|
-
import com.cloudflare.realtimekit.ForegroundService;
|
|
15
12
|
import com.facebook.react.bridge.Callback;
|
|
16
13
|
import com.facebook.react.bridge.LifecycleEventListener;
|
|
17
14
|
import com.facebook.react.bridge.Promise;
|
|
@@ -35,6 +32,7 @@ import com.cloudflare.realtimekit.RTKHolder;
|
|
|
35
32
|
@ReactModule(name = "RTKHelper")
|
|
36
33
|
public class RTKHelperModule extends ReactContextBaseJavaModule {
|
|
37
34
|
|
|
35
|
+
private static final String TAG = "RTKHelperModule";
|
|
38
36
|
private final ReactApplicationContext reactContext;
|
|
39
37
|
private static final String ERROR_INVALID_ACTIVITY = "E_INVALID_ACTIVITY";
|
|
40
38
|
|
|
@@ -59,9 +57,8 @@ public class RTKHelperModule extends ReactContextBaseJavaModule {
|
|
|
59
57
|
public void onHostDestroy() {
|
|
60
58
|
if (wakeLock.isHeld())
|
|
61
59
|
wakeLock.release();
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
getReactApplicationContext().stopService(intent);
|
|
60
|
+
// MediaProjectionService is owned by react-native-webrtc and stops itself
|
|
61
|
+
// via MediaProjection.Callback.onStop() — no explicit stop needed here.
|
|
65
62
|
}
|
|
66
63
|
};
|
|
67
64
|
// react-native-background-timer end
|
|
@@ -90,59 +87,6 @@ public class RTKHelperModule extends ReactContextBaseJavaModule {
|
|
|
90
87
|
return "RTKHelper";
|
|
91
88
|
}
|
|
92
89
|
|
|
93
|
-
@ReactMethod
|
|
94
|
-
public void createNotificationChannel(Promise promise) {
|
|
95
|
-
NotificationHelper.getInstance(getReactApplicationContext()).createNotificationChannel(promise);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
@ReactMethod
|
|
99
|
-
public void startService(Promise promise) {
|
|
100
|
-
ComponentName mediaService;
|
|
101
|
-
RTKHolder.currActivity = this.getCurrentActivity();
|
|
102
|
-
Intent intent = new Intent(getReactApplicationContext(), ForegroundService.class);
|
|
103
|
-
intent.setAction(Constants.ACTION_FOREGROUND_SERVICE_START);
|
|
104
|
-
try{
|
|
105
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
106
|
-
mediaService = getReactApplicationContext().startForegroundService(intent);
|
|
107
|
-
} else {
|
|
108
|
-
mediaService = getReactApplicationContext().startService(intent);
|
|
109
|
-
}
|
|
110
|
-
} catch (RuntimeException e) {
|
|
111
|
-
// Avoid crashing due to ForegroundServiceStartNotAllowedException (API level 31).
|
|
112
|
-
// See: https://developer.android.com/guide/components/foreground-services#background-start-restrictions
|
|
113
|
-
RTKLogger.w("RTKHelper", "Media projection service not started: " + e.getMessage());
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
if (mediaService != null) {
|
|
117
|
-
promise.resolve(null);
|
|
118
|
-
} else {
|
|
119
|
-
promise.reject(Constants.ERROR_SERVICE_ERROR, "RTKForegroundService: Foreground service is not started");
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
@ReactMethod
|
|
124
|
-
public void stopService(Promise promise) {
|
|
125
|
-
Intent intent = new Intent(getReactApplicationContext(), ForegroundService.class);
|
|
126
|
-
intent.setAction(Constants.ACTION_FOREGROUND_SERVICE_STOP);
|
|
127
|
-
boolean stopped = getReactApplicationContext().stopService(intent);
|
|
128
|
-
if (stopped) {
|
|
129
|
-
promise.resolve(null);
|
|
130
|
-
} else {
|
|
131
|
-
promise.reject(Constants.ERROR_SERVICE_ERROR, "RTKForegroundService: Foreground service failed to stop");
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
@ReactMethod
|
|
135
|
-
private void isForegroundServiceRunning(Promise promise) {
|
|
136
|
-
ActivityManager manager = (ActivityManager) this.getCurrentActivity().getSystemService(getReactApplicationContext().ACTIVITY_SERVICE);
|
|
137
|
-
for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
|
|
138
|
-
if ((ForegroundService.class).getName().equals(service.service.getClassName())) {
|
|
139
|
-
promise.resolve(true);
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
promise.resolve(false);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
90
|
@ReactMethod
|
|
147
91
|
public void activateScreenWake() {
|
|
148
92
|
final Activity activity = getCurrentActivity();
|
|
@@ -255,6 +199,29 @@ public class RTKHelperModule extends ReactContextBaseJavaModule {
|
|
|
255
199
|
.emit(eventName, null);
|
|
256
200
|
}
|
|
257
201
|
|
|
202
|
+
@ReactMethod
|
|
203
|
+
public void startMeetingService(ReadableMap config, Promise promise) {
|
|
204
|
+
ReactApplicationContext context = getReactApplicationContext();
|
|
205
|
+
String title = config.hasKey("title") ? config.getString("title") : null;
|
|
206
|
+
String text = config.hasKey("text") ? config.getString("text") : null;
|
|
207
|
+
String channelId = config.hasKey("channelId") ? config.getString("channelId") : null;
|
|
208
|
+
String channelName = config.hasKey("channelName") ? config.getString("channelName") : null;
|
|
209
|
+
try {
|
|
210
|
+
KeepAliveService.launch(context, title, text, channelId, channelName);
|
|
211
|
+
promise.resolve(null);
|
|
212
|
+
} catch (RuntimeException e) {
|
|
213
|
+
// ForegroundServiceStartNotAllowedException or similar — not fatal.
|
|
214
|
+
Log.w(TAG, "startMeetingService failed: " + e.getMessage());
|
|
215
|
+
promise.resolve(null); // best-effort, never reject
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@ReactMethod
|
|
220
|
+
public void stopMeetingService(Promise promise) {
|
|
221
|
+
KeepAliveService.stop(getReactApplicationContext());
|
|
222
|
+
promise.resolve(null);
|
|
223
|
+
}
|
|
224
|
+
|
|
258
225
|
@ReactMethod
|
|
259
226
|
public void backgroundTimerSetTimeout(final int id, final double timeout) {
|
|
260
227
|
Handler handler = new Handler();
|
|
@@ -25,6 +25,19 @@ const {
|
|
|
25
25
|
} = _reactNative.NativeModules;
|
|
26
26
|
const broadcastEmitter = exports.broadcastEmitter = new _reactNative.NativeEventEmitter(BroadcastEventEmitter);
|
|
27
27
|
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Meeting foreground service config
|
|
30
|
+
// Set by LocalMediaHandler.configure() before the first init() call.
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
let _meetingServiceConfig = {
|
|
34
|
+
enabled: true,
|
|
35
|
+
title: 'In a call',
|
|
36
|
+
text: 'Tap to return to your meeting',
|
|
37
|
+
channelId: 'rtk_meeting',
|
|
38
|
+
channelName: 'Ongoing Meeting'
|
|
39
|
+
};
|
|
40
|
+
|
|
28
41
|
// eslint-disable-next-line no-shadow
|
|
29
42
|
let MediaEvents = exports.MediaEvents = /*#__PURE__*/function (MediaEvents) {
|
|
30
43
|
MediaEvents[MediaEvents["AUDIO_TRACK_CHANGE"] = 0] = "AUDIO_TRACK_CHANGE";
|
|
@@ -51,26 +64,6 @@ var _broadcastEmitter = /*#__PURE__*/new WeakMap();
|
|
|
51
64
|
var _broadcastStartedSubscription = /*#__PURE__*/new WeakMap();
|
|
52
65
|
var _broadcastStoppedSubscription = /*#__PURE__*/new WeakMap();
|
|
53
66
|
class LocalMediaHandler extends _events.EventEmitter {
|
|
54
|
-
async configureForeground() {
|
|
55
|
-
if (_reactNative.Platform.OS !== 'android') return;
|
|
56
|
-
const val = await RTKHelper.isForegroundServiceRunning();
|
|
57
|
-
if (!val) {
|
|
58
|
-
if (_reactNative.Platform.Version >= 33) {
|
|
59
|
-
const res = await _reactNative.PermissionsAndroid.request(_reactNative.PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS);
|
|
60
|
-
if (res !== _reactNative.PermissionsAndroid.RESULTS.GRANTED) {
|
|
61
|
-
console.warn('Permission required for screenshare notification');
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
if (_reactNative.Platform.Version >= 26) {
|
|
65
|
-
await RTKHelper.createNotificationChannel();
|
|
66
|
-
}
|
|
67
|
-
try {
|
|
68
|
-
await RTKHelper.startService();
|
|
69
|
-
} catch (e) {
|
|
70
|
-
console.warn('Failed to start foreground service', e);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
67
|
constructor(localMediaUtils) {
|
|
75
68
|
super();
|
|
76
69
|
_classPrivateFieldInitSpec(this, _localMediaUtils, void 0);
|
|
@@ -94,9 +87,15 @@ class LocalMediaHandler extends _events.EventEmitter {
|
|
|
94
87
|
_classPrivateFieldInitSpec(this, _broadcastStartedSubscription, void 0);
|
|
95
88
|
_classPrivateFieldInitSpec(this, _broadcastStoppedSubscription, void 0);
|
|
96
89
|
_defineProperty(this, "_handleAppStateChange", nextAppState => {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
90
|
+
const prevAppState = _classPrivateFieldGet(_appState, this);
|
|
91
|
+
_classPrivateFieldSet(_appState, this, nextAppState);
|
|
92
|
+
if (prevAppState.match(/inactive|background/) && nextAppState === 'active') {
|
|
93
|
+
// App returned to foreground — stop the background keep-alive timer
|
|
94
|
+
// and kick the video track to recover the camera feed.
|
|
95
|
+
if (_classPrivateFieldGet(_interval, this) !== null) {
|
|
96
|
+
_BackgroundHandler.default.clearInterval(_classPrivateFieldGet(_interval, this));
|
|
97
|
+
_classPrivateFieldSet(_interval, this, null);
|
|
98
|
+
}
|
|
100
99
|
if (this.videoTrack) {
|
|
101
100
|
this.videoTrack.enabled = false;
|
|
102
101
|
setTimeout(() => {
|
|
@@ -105,16 +104,23 @@ class LocalMediaHandler extends _events.EventEmitter {
|
|
|
105
104
|
}
|
|
106
105
|
}, 100);
|
|
107
106
|
}
|
|
108
|
-
} else {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
} else if (nextAppState.match(/inactive|background/)) {
|
|
108
|
+
// App moved to background — start a recurring no-op timer to keep the
|
|
109
|
+
// JS thread alive so WebRTC/native events continue to be processed.
|
|
110
|
+
// Clear any existing interval first to avoid accumulation.
|
|
111
|
+
if (_classPrivateFieldGet(_interval, this) !== null) {
|
|
112
|
+
_BackgroundHandler.default.clearInterval(_classPrivateFieldGet(_interval, this));
|
|
113
113
|
}
|
|
114
114
|
_classPrivateFieldSet(_interval, this, _BackgroundHandler.default.setInterval(() => {}, 1000));
|
|
115
|
-
_classPrivateFieldSet(_appState, this, nextAppState);
|
|
116
115
|
}
|
|
117
116
|
});
|
|
117
|
+
// Arrow function so `this` is stable — the same reference is used in both
|
|
118
|
+
// addEventListener (setScreenShareTracks) and removeEventListener
|
|
119
|
+
// (removeScreenShareTracks). A regular method + .bind(this) creates a new
|
|
120
|
+
// function object on every call, making removeEventListener a no-op.
|
|
121
|
+
_defineProperty(this, "onScreenShareEnded", () => {
|
|
122
|
+
this.emit('SCREENSHARE_ENDED');
|
|
123
|
+
});
|
|
118
124
|
_classPrivateFieldSet(_localMediaUtils, this, localMediaUtils);
|
|
119
125
|
_classPrivateFieldSet(_broadcastEmitter, this, broadcastEmitter);
|
|
120
126
|
_classPrivateFieldSet(_appState, this, '');
|
|
@@ -492,6 +498,11 @@ class LocalMediaHandler extends _events.EventEmitter {
|
|
|
492
498
|
if (_reactNative.Platform.OS === 'android') {
|
|
493
499
|
_classPrivateFieldSet(_appStateSubscription, this, _reactNative.AppState.addEventListener('change', this._handleAppStateChange));
|
|
494
500
|
}
|
|
501
|
+
|
|
502
|
+
// Start the meeting foreground service so Android keeps the app alive
|
|
503
|
+
// in the background during a call. The app is guaranteed to be in the
|
|
504
|
+
// foreground at this point (user just tapped join / setup screen).
|
|
505
|
+
await this.startMeetingService();
|
|
495
506
|
}
|
|
496
507
|
stopScreenShareTracks(tracks = {}) {
|
|
497
508
|
const {
|
|
@@ -499,20 +510,22 @@ class LocalMediaHandler extends _events.EventEmitter {
|
|
|
499
510
|
video = true
|
|
500
511
|
} = tracks;
|
|
501
512
|
if (audio) {
|
|
502
|
-
var _this$screenShareTrac;
|
|
513
|
+
var _this$screenShareTrac, _this$screenShareTrac2;
|
|
503
514
|
(_this$screenShareTrac = this.screenShareTracks) === null || _this$screenShareTrac === void 0 || (_this$screenShareTrac = _this$screenShareTrac.audio) === null || _this$screenShareTrac === void 0 || _this$screenShareTrac.stop();
|
|
515
|
+
(_this$screenShareTrac2 = this.screenShareTracks) === null || _this$screenShareTrac2 === void 0 || (_this$screenShareTrac2 = _this$screenShareTrac2.audio) === null || _this$screenShareTrac2 === void 0 || _this$screenShareTrac2.release();
|
|
504
516
|
}
|
|
505
517
|
if (video) {
|
|
506
|
-
var _this$
|
|
507
|
-
(_this$
|
|
518
|
+
var _this$screenShareTrac3, _this$screenShareTrac4;
|
|
519
|
+
(_this$screenShareTrac3 = this.screenShareTracks) === null || _this$screenShareTrac3 === void 0 || (_this$screenShareTrac3 = _this$screenShareTrac3.video) === null || _this$screenShareTrac3 === void 0 || _this$screenShareTrac3.stop();
|
|
520
|
+
(_this$screenShareTrac4 = this.screenShareTracks) === null || _this$screenShareTrac4 === void 0 || (_this$screenShareTrac4 = _this$screenShareTrac4.video) === null || _this$screenShareTrac4 === void 0 || _this$screenShareTrac4.release();
|
|
508
521
|
}
|
|
509
522
|
if (audio && video) {
|
|
510
523
|
this.screenShareEnabled = false;
|
|
511
524
|
}
|
|
512
525
|
}
|
|
513
526
|
removeScreenShareTracks(tracks) {
|
|
514
|
-
var _this$
|
|
515
|
-
(_this$
|
|
527
|
+
var _this$screenShareTrac5;
|
|
528
|
+
(_this$screenShareTrac5 = this.screenShareTracks.video) === null || _this$screenShareTrac5 === void 0 || _this$screenShareTrac5.removeEventListener('ended', this.onScreenShareEnded);
|
|
516
529
|
this.stopScreenShareTracks(tracks);
|
|
517
530
|
this.screenShareTracks = {
|
|
518
531
|
audio: undefined,
|
|
@@ -544,7 +557,7 @@ class LocalMediaHandler extends _events.EventEmitter {
|
|
|
544
557
|
this.videoTrack = videoTrack;
|
|
545
558
|
}
|
|
546
559
|
setScreenShareTracks(tracks) {
|
|
547
|
-
var _this$
|
|
560
|
+
var _this$screenShareTrac6;
|
|
548
561
|
const {
|
|
549
562
|
audio,
|
|
550
563
|
video
|
|
@@ -556,10 +569,7 @@ class LocalMediaHandler extends _events.EventEmitter {
|
|
|
556
569
|
});
|
|
557
570
|
}
|
|
558
571
|
this.screenShareTracks = tracks;
|
|
559
|
-
(_this$
|
|
560
|
-
}
|
|
561
|
-
onScreenShareEnded() {
|
|
562
|
-
this.emit('SCREENSHARE_ENDED');
|
|
572
|
+
(_this$screenShareTrac6 = this.screenShareTracks.video) === null || _this$screenShareTrac6 === void 0 || _this$screenShareTrac6.addEventListener('ended', this.onScreenShareEnded);
|
|
563
573
|
}
|
|
564
574
|
async disableAudio() {
|
|
565
575
|
await this.toggleAudio();
|
|
@@ -685,53 +695,19 @@ class LocalMediaHandler extends _events.EventEmitter {
|
|
|
685
695
|
return;
|
|
686
696
|
}
|
|
687
697
|
}
|
|
688
|
-
async
|
|
689
|
-
if
|
|
690
|
-
|
|
691
|
-
try {
|
|
692
|
-
const val = await RTKHelper.isForegroundServiceRunning();
|
|
693
|
-
if (val) {
|
|
694
|
-
await RTKHelper.stopService();
|
|
695
|
-
}
|
|
696
|
-
} catch (e) {
|
|
697
|
-
console.warn('Failed to stop foreground service', e);
|
|
698
|
-
}
|
|
699
|
-
return;
|
|
700
|
-
}
|
|
701
|
-
if (_reactNative.Platform.OS === 'ios' && this.screenShareEnabled) {
|
|
702
|
-
_reactNative.NativeModules.RTKScreensharePickerView.showScreenSharePickerView();
|
|
703
|
-
_classPrivateFieldSet(_screenShareState, this, 'PENDING');
|
|
704
|
-
await new Promise((resolve, reject) => {
|
|
705
|
-
let timeoutId;
|
|
706
|
-
let time = 0;
|
|
707
|
-
timeoutId = setInterval(() => {
|
|
708
|
-
if (_classPrivateFieldGet(_screenShareState, this) === 'STOPPED') {
|
|
709
|
-
clearInterval(timeoutId);
|
|
710
|
-
resolve();
|
|
711
|
-
}
|
|
712
|
-
time = time + 100;
|
|
713
|
-
if (time > 20000) {
|
|
714
|
-
clearInterval(timeoutId);
|
|
715
|
-
reject('Timeout exceeded for disableScreenShare()');
|
|
716
|
-
}
|
|
717
|
-
}, 100);
|
|
718
|
-
});
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
698
|
+
async enableScreenShare() {
|
|
699
|
+
// Guard: do nothing if already sharing or a consent dialog is in flight.
|
|
700
|
+
if (this.screenShareEnabled || this._pendingScreenShare) return;
|
|
721
701
|
if (_reactNative.Platform.OS === 'android') {
|
|
722
702
|
this._pendingScreenShare = true;
|
|
723
|
-
await this.configureForeground();
|
|
724
703
|
try {
|
|
704
|
+
// getScreenShareTracks() manages the full flow:
|
|
705
|
+
// consent dialog → start MediaProjectionService → create MediaStream
|
|
725
706
|
this.setScreenShareTracks(await _LocalMediaUtils.default.getScreenShareTracks());
|
|
726
707
|
this.screenShareEnabled = true;
|
|
727
708
|
} catch (e) {
|
|
728
709
|
console.warn('Failed to start screenshare', e);
|
|
729
710
|
this.screenShareEnabled = false;
|
|
730
|
-
const val = await RTKHelper.isForegroundServiceRunning();
|
|
731
|
-
if (val) {
|
|
732
|
-
await RTKHelper.stopService();
|
|
733
|
-
}
|
|
734
|
-
this._pendingScreenShare = false;
|
|
735
711
|
} finally {
|
|
736
712
|
this._pendingScreenShare = false;
|
|
737
713
|
}
|
|
@@ -756,11 +732,32 @@ class LocalMediaHandler extends _events.EventEmitter {
|
|
|
756
732
|
});
|
|
757
733
|
}
|
|
758
734
|
}
|
|
759
|
-
async enableScreenShare() {
|
|
760
|
-
await this.toggleScreenShare();
|
|
761
|
-
}
|
|
762
735
|
async disableScreenShare() {
|
|
763
|
-
|
|
736
|
+
// Guard: nothing to stop if not sharing and no consent dialog is pending.
|
|
737
|
+
if (!this.screenShareEnabled && !this._pendingScreenShare) return;
|
|
738
|
+
if (_reactNative.Platform.OS !== 'ios') {
|
|
739
|
+
this.removeScreenShareTracks();
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// iOS: trigger the broadcast picker to stop the extension.
|
|
744
|
+
_reactNative.NativeModules.RTKScreensharePickerView.showScreenSharePickerView();
|
|
745
|
+
_classPrivateFieldSet(_screenShareState, this, 'PENDING');
|
|
746
|
+
await new Promise((resolve, reject) => {
|
|
747
|
+
let timeoutId;
|
|
748
|
+
let time = 0;
|
|
749
|
+
timeoutId = setInterval(() => {
|
|
750
|
+
if (_classPrivateFieldGet(_screenShareState, this) === 'STOPPED') {
|
|
751
|
+
clearInterval(timeoutId);
|
|
752
|
+
resolve();
|
|
753
|
+
}
|
|
754
|
+
time = time + 100;
|
|
755
|
+
if (time > 20000) {
|
|
756
|
+
clearInterval(timeoutId);
|
|
757
|
+
reject('Timeout exceeded for disableScreenShare()');
|
|
758
|
+
}
|
|
759
|
+
}, 100);
|
|
760
|
+
});
|
|
764
761
|
}
|
|
765
762
|
getAllDevices() {
|
|
766
763
|
return _classPrivateFieldGet(_localMediaUtils, this).availableDevices;
|
|
@@ -887,10 +884,45 @@ class LocalMediaHandler extends _events.EventEmitter {
|
|
|
887
884
|
this.emit('DEVICE_LIST_UPDATED', changedDevices);
|
|
888
885
|
}
|
|
889
886
|
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Override the meeting foreground service config before the first meeting
|
|
890
|
+
* session. Called by useRealtimeKitClient when keepAliveService is set.
|
|
891
|
+
*/
|
|
892
|
+
static configure(config) {
|
|
893
|
+
_meetingServiceConfig = {
|
|
894
|
+
..._meetingServiceConfig,
|
|
895
|
+
...config
|
|
896
|
+
};
|
|
897
|
+
}
|
|
890
898
|
static async init(_) {
|
|
891
899
|
const localMediaUtils = await _LocalMediaUtils.default.init();
|
|
892
900
|
return new LocalMediaHandler(localMediaUtils);
|
|
893
901
|
}
|
|
902
|
+
async startMeetingService() {
|
|
903
|
+
if (_reactNative.Platform.OS !== 'android') return;
|
|
904
|
+
if (!_meetingServiceConfig.enabled) return;
|
|
905
|
+
|
|
906
|
+
// Android 13+ (API 33): request POST_NOTIFICATIONS so the foreground-service
|
|
907
|
+
// notification appears in the shade.
|
|
908
|
+
if (Number(_reactNative.Platform.Version) >= 33) {
|
|
909
|
+
try {
|
|
910
|
+
await _reactNative.PermissionsAndroid.request(_reactNative.PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS);
|
|
911
|
+
} catch (e) {
|
|
912
|
+
console.warn('Failed to request POST_NOTIFICATIONS permission', e);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
try {
|
|
916
|
+
await RTKHelper.startMeetingService({
|
|
917
|
+
title: _meetingServiceConfig.title,
|
|
918
|
+
text: _meetingServiceConfig.text,
|
|
919
|
+
channelId: _meetingServiceConfig.channelId,
|
|
920
|
+
channelName: _meetingServiceConfig.channelName
|
|
921
|
+
});
|
|
922
|
+
} catch (e) {
|
|
923
|
+
console.warn('Failed to start KeepAliveService', e);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
894
926
|
emit(event, ...args) {
|
|
895
927
|
return super.emit(event, ...args);
|
|
896
928
|
}
|