@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.
Files changed (30) hide show
  1. package/android/src/main/AndroidManifest.xml +6 -1
  2. package/android/src/main/java/com/cloudflare/realtimekit/KeepAliveService.java +176 -0
  3. package/android/src/main/java/com/cloudflare/realtimekit/RTKHelperModule.java +27 -60
  4. package/lib/commonjs/LocalMediaHandler.js +114 -82
  5. package/lib/commonjs/LocalMediaHandler.js.map +1 -1
  6. package/lib/commonjs/LocalMediaUtils.js +3 -5
  7. package/lib/commonjs/LocalMediaUtils.js.map +1 -1
  8. package/lib/commonjs/ReactHooks.js +27 -9
  9. package/lib/commonjs/ReactHooks.js.map +1 -1
  10. package/lib/commonjs/index.js.map +1 -1
  11. package/lib/commonjs/utils/version.js +1 -1
  12. package/lib/commonjs/utils/version.js.map +1 -1
  13. package/lib/module/LocalMediaHandler.js +114 -82
  14. package/lib/module/LocalMediaHandler.js.map +1 -1
  15. package/lib/module/LocalMediaUtils.js +3 -5
  16. package/lib/module/LocalMediaUtils.js.map +1 -1
  17. package/lib/module/ReactHooks.js +27 -8
  18. package/lib/module/ReactHooks.js.map +1 -1
  19. package/lib/module/index.js.map +1 -1
  20. package/lib/module/utils/version.js +1 -1
  21. package/lib/module/utils/version.js.map +1 -1
  22. package/lib/typescript/LocalMediaHandler.d.ts +18 -4
  23. package/lib/typescript/ReactHooks.d.ts +18 -0
  24. package/lib/typescript/index.d.ts +1 -0
  25. package/lib/typescript/utils/version.d.ts +1 -1
  26. package/package.json +1 -1
  27. package/plugin/build/withRTK.js +50 -22
  28. package/plugin/src/withRTK.ts +58 -23
  29. package/android/src/main/java/com/cloudflare/realtimekit/ForegroundService.java +0 -46
  30. 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.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
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
- Intent intent = new Intent(getReactApplicationContext(), ForegroundService.class);
63
- intent.setAction(Constants.ACTION_FOREGROUND_SERVICE_STOP);
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
- if (_classPrivateFieldGet(_appState, this).match(/inactive|background/) && nextAppState === 'active') {
98
- _BackgroundHandler.default.clearInterval(_classPrivateFieldGet(_interval, this));
99
- _classPrivateFieldSet(_interval, this, null);
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
- if (_classPrivateFieldGet(_interval, this) === null) {
110
- if (this.screenShareEnabled || this._pendingScreenShare) {
111
- this.configureForeground();
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$screenShareTrac2;
507
- (_this$screenShareTrac2 = this.screenShareTracks) === null || _this$screenShareTrac2 === void 0 || (_this$screenShareTrac2 = _this$screenShareTrac2.video) === null || _this$screenShareTrac2 === void 0 || _this$screenShareTrac2.stop();
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$screenShareTrac3;
515
- (_this$screenShareTrac3 = this.screenShareTracks.video) === null || _this$screenShareTrac3 === void 0 || _this$screenShareTrac3.removeEventListener('ended', this.onScreenShareEnded);
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$screenShareTrac4;
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$screenShareTrac4 = this.screenShareTracks.video) === null || _this$screenShareTrac4 === void 0 || _this$screenShareTrac4.addEventListener('ended', this.onScreenShareEnded.bind(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 toggleScreenShare() {
689
- if (this.screenShareEnabled && _reactNative.Platform.OS !== 'ios') {
690
- this.removeScreenShareTracks();
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
- await this.toggleScreenShare();
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
  }