@cloudflare/realtimekit-react-native 1.1.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/README.md +2 -2
- 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/ReactContext.js +33 -33
- package/lib/commonjs/ReactContext.js.map +1 -1
- package/lib/commonjs/ReactHooks.js +71 -15
- 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/ReactContext.js +33 -33
- package/lib/module/ReactContext.js.map +1 -1
- package/lib/module/ReactHooks.js +71 -15
- 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 +22 -1
- package/lib/typescript/index.d.ts +1 -0
- package/lib/typescript/utils/version.d.ts +1 -1
- package/package.json +3 -5
- 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
|
@@ -19,6 +19,19 @@ const {
|
|
|
19
19
|
} = NativeModules;
|
|
20
20
|
export const broadcastEmitter = new NativeEventEmitter(BroadcastEventEmitter);
|
|
21
21
|
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Meeting foreground service config
|
|
24
|
+
// Set by LocalMediaHandler.configure() before the first init() call.
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
let _meetingServiceConfig = {
|
|
28
|
+
enabled: true,
|
|
29
|
+
title: 'In a call',
|
|
30
|
+
text: 'Tap to return to your meeting',
|
|
31
|
+
channelId: 'rtk_meeting',
|
|
32
|
+
channelName: 'Ongoing Meeting'
|
|
33
|
+
};
|
|
34
|
+
|
|
22
35
|
// eslint-disable-next-line no-shadow
|
|
23
36
|
export let MediaEvents = /*#__PURE__*/function (MediaEvents) {
|
|
24
37
|
MediaEvents[MediaEvents["AUDIO_TRACK_CHANGE"] = 0] = "AUDIO_TRACK_CHANGE";
|
|
@@ -45,26 +58,6 @@ var _broadcastEmitter = /*#__PURE__*/new WeakMap();
|
|
|
45
58
|
var _broadcastStartedSubscription = /*#__PURE__*/new WeakMap();
|
|
46
59
|
var _broadcastStoppedSubscription = /*#__PURE__*/new WeakMap();
|
|
47
60
|
export default class LocalMediaHandler extends EventEmitter {
|
|
48
|
-
async configureForeground() {
|
|
49
|
-
if (Platform.OS !== 'android') return;
|
|
50
|
-
const val = await RTKHelper.isForegroundServiceRunning();
|
|
51
|
-
if (!val) {
|
|
52
|
-
if (Platform.Version >= 33) {
|
|
53
|
-
const res = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS);
|
|
54
|
-
if (res !== PermissionsAndroid.RESULTS.GRANTED) {
|
|
55
|
-
console.warn('Permission required for screenshare notification');
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
if (Platform.Version >= 26) {
|
|
59
|
-
await RTKHelper.createNotificationChannel();
|
|
60
|
-
}
|
|
61
|
-
try {
|
|
62
|
-
await RTKHelper.startService();
|
|
63
|
-
} catch (e) {
|
|
64
|
-
console.warn('Failed to start foreground service', e);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
61
|
constructor(localMediaUtils) {
|
|
69
62
|
super();
|
|
70
63
|
_classPrivateFieldInitSpec(this, _localMediaUtils, void 0);
|
|
@@ -88,9 +81,15 @@ export default class LocalMediaHandler extends EventEmitter {
|
|
|
88
81
|
_classPrivateFieldInitSpec(this, _broadcastStartedSubscription, void 0);
|
|
89
82
|
_classPrivateFieldInitSpec(this, _broadcastStoppedSubscription, void 0);
|
|
90
83
|
_defineProperty(this, "_handleAppStateChange", nextAppState => {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
84
|
+
const prevAppState = _classPrivateFieldGet(_appState, this);
|
|
85
|
+
_classPrivateFieldSet(_appState, this, nextAppState);
|
|
86
|
+
if (prevAppState.match(/inactive|background/) && nextAppState === 'active') {
|
|
87
|
+
// App returned to foreground — stop the background keep-alive timer
|
|
88
|
+
// and kick the video track to recover the camera feed.
|
|
89
|
+
if (_classPrivateFieldGet(_interval, this) !== null) {
|
|
90
|
+
BackgroundTimer.clearInterval(_classPrivateFieldGet(_interval, this));
|
|
91
|
+
_classPrivateFieldSet(_interval, this, null);
|
|
92
|
+
}
|
|
94
93
|
if (this.videoTrack) {
|
|
95
94
|
this.videoTrack.enabled = false;
|
|
96
95
|
setTimeout(() => {
|
|
@@ -99,16 +98,23 @@ export default class LocalMediaHandler extends EventEmitter {
|
|
|
99
98
|
}
|
|
100
99
|
}, 100);
|
|
101
100
|
}
|
|
102
|
-
} else {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
101
|
+
} else if (nextAppState.match(/inactive|background/)) {
|
|
102
|
+
// App moved to background — start a recurring no-op timer to keep the
|
|
103
|
+
// JS thread alive so WebRTC/native events continue to be processed.
|
|
104
|
+
// Clear any existing interval first to avoid accumulation.
|
|
105
|
+
if (_classPrivateFieldGet(_interval, this) !== null) {
|
|
106
|
+
BackgroundTimer.clearInterval(_classPrivateFieldGet(_interval, this));
|
|
107
107
|
}
|
|
108
108
|
_classPrivateFieldSet(_interval, this, BackgroundTimer.setInterval(() => {}, 1000));
|
|
109
|
-
_classPrivateFieldSet(_appState, this, nextAppState);
|
|
110
109
|
}
|
|
111
110
|
});
|
|
111
|
+
// Arrow function so `this` is stable — the same reference is used in both
|
|
112
|
+
// addEventListener (setScreenShareTracks) and removeEventListener
|
|
113
|
+
// (removeScreenShareTracks). A regular method + .bind(this) creates a new
|
|
114
|
+
// function object on every call, making removeEventListener a no-op.
|
|
115
|
+
_defineProperty(this, "onScreenShareEnded", () => {
|
|
116
|
+
this.emit('SCREENSHARE_ENDED');
|
|
117
|
+
});
|
|
112
118
|
_classPrivateFieldSet(_localMediaUtils, this, localMediaUtils);
|
|
113
119
|
_classPrivateFieldSet(_broadcastEmitter, this, broadcastEmitter);
|
|
114
120
|
_classPrivateFieldSet(_appState, this, '');
|
|
@@ -486,6 +492,11 @@ export default class LocalMediaHandler extends EventEmitter {
|
|
|
486
492
|
if (Platform.OS === 'android') {
|
|
487
493
|
_classPrivateFieldSet(_appStateSubscription, this, AppState.addEventListener('change', this._handleAppStateChange));
|
|
488
494
|
}
|
|
495
|
+
|
|
496
|
+
// Start the meeting foreground service so Android keeps the app alive
|
|
497
|
+
// in the background during a call. The app is guaranteed to be in the
|
|
498
|
+
// foreground at this point (user just tapped join / setup screen).
|
|
499
|
+
await this.startMeetingService();
|
|
489
500
|
}
|
|
490
501
|
stopScreenShareTracks(tracks = {}) {
|
|
491
502
|
const {
|
|
@@ -493,20 +504,22 @@ export default class LocalMediaHandler extends EventEmitter {
|
|
|
493
504
|
video = true
|
|
494
505
|
} = tracks;
|
|
495
506
|
if (audio) {
|
|
496
|
-
var _this$screenShareTrac;
|
|
507
|
+
var _this$screenShareTrac, _this$screenShareTrac2;
|
|
497
508
|
(_this$screenShareTrac = this.screenShareTracks) === null || _this$screenShareTrac === void 0 || (_this$screenShareTrac = _this$screenShareTrac.audio) === null || _this$screenShareTrac === void 0 || _this$screenShareTrac.stop();
|
|
509
|
+
(_this$screenShareTrac2 = this.screenShareTracks) === null || _this$screenShareTrac2 === void 0 || (_this$screenShareTrac2 = _this$screenShareTrac2.audio) === null || _this$screenShareTrac2 === void 0 || _this$screenShareTrac2.release();
|
|
498
510
|
}
|
|
499
511
|
if (video) {
|
|
500
|
-
var _this$
|
|
501
|
-
(_this$
|
|
512
|
+
var _this$screenShareTrac3, _this$screenShareTrac4;
|
|
513
|
+
(_this$screenShareTrac3 = this.screenShareTracks) === null || _this$screenShareTrac3 === void 0 || (_this$screenShareTrac3 = _this$screenShareTrac3.video) === null || _this$screenShareTrac3 === void 0 || _this$screenShareTrac3.stop();
|
|
514
|
+
(_this$screenShareTrac4 = this.screenShareTracks) === null || _this$screenShareTrac4 === void 0 || (_this$screenShareTrac4 = _this$screenShareTrac4.video) === null || _this$screenShareTrac4 === void 0 || _this$screenShareTrac4.release();
|
|
502
515
|
}
|
|
503
516
|
if (audio && video) {
|
|
504
517
|
this.screenShareEnabled = false;
|
|
505
518
|
}
|
|
506
519
|
}
|
|
507
520
|
removeScreenShareTracks(tracks) {
|
|
508
|
-
var _this$
|
|
509
|
-
(_this$
|
|
521
|
+
var _this$screenShareTrac5;
|
|
522
|
+
(_this$screenShareTrac5 = this.screenShareTracks.video) === null || _this$screenShareTrac5 === void 0 || _this$screenShareTrac5.removeEventListener('ended', this.onScreenShareEnded);
|
|
510
523
|
this.stopScreenShareTracks(tracks);
|
|
511
524
|
this.screenShareTracks = {
|
|
512
525
|
audio: undefined,
|
|
@@ -538,7 +551,7 @@ export default class LocalMediaHandler extends EventEmitter {
|
|
|
538
551
|
this.videoTrack = videoTrack;
|
|
539
552
|
}
|
|
540
553
|
setScreenShareTracks(tracks) {
|
|
541
|
-
var _this$
|
|
554
|
+
var _this$screenShareTrac6;
|
|
542
555
|
const {
|
|
543
556
|
audio,
|
|
544
557
|
video
|
|
@@ -550,10 +563,7 @@ export default class LocalMediaHandler extends EventEmitter {
|
|
|
550
563
|
});
|
|
551
564
|
}
|
|
552
565
|
this.screenShareTracks = tracks;
|
|
553
|
-
(_this$
|
|
554
|
-
}
|
|
555
|
-
onScreenShareEnded() {
|
|
556
|
-
this.emit('SCREENSHARE_ENDED');
|
|
566
|
+
(_this$screenShareTrac6 = this.screenShareTracks.video) === null || _this$screenShareTrac6 === void 0 || _this$screenShareTrac6.addEventListener('ended', this.onScreenShareEnded);
|
|
557
567
|
}
|
|
558
568
|
async disableAudio() {
|
|
559
569
|
await this.toggleAudio();
|
|
@@ -679,53 +689,19 @@ export default class LocalMediaHandler extends EventEmitter {
|
|
|
679
689
|
return;
|
|
680
690
|
}
|
|
681
691
|
}
|
|
682
|
-
async
|
|
683
|
-
if
|
|
684
|
-
|
|
685
|
-
try {
|
|
686
|
-
const val = await RTKHelper.isForegroundServiceRunning();
|
|
687
|
-
if (val) {
|
|
688
|
-
await RTKHelper.stopService();
|
|
689
|
-
}
|
|
690
|
-
} catch (e) {
|
|
691
|
-
console.warn('Failed to stop foreground service', e);
|
|
692
|
-
}
|
|
693
|
-
return;
|
|
694
|
-
}
|
|
695
|
-
if (Platform.OS === 'ios' && this.screenShareEnabled) {
|
|
696
|
-
NativeModules.RTKScreensharePickerView.showScreenSharePickerView();
|
|
697
|
-
_classPrivateFieldSet(_screenShareState, this, 'PENDING');
|
|
698
|
-
await new Promise((resolve, reject) => {
|
|
699
|
-
let timeoutId;
|
|
700
|
-
let time = 0;
|
|
701
|
-
timeoutId = setInterval(() => {
|
|
702
|
-
if (_classPrivateFieldGet(_screenShareState, this) === 'STOPPED') {
|
|
703
|
-
clearInterval(timeoutId);
|
|
704
|
-
resolve();
|
|
705
|
-
}
|
|
706
|
-
time = time + 100;
|
|
707
|
-
if (time > 20000) {
|
|
708
|
-
clearInterval(timeoutId);
|
|
709
|
-
reject('Timeout exceeded for disableScreenShare()');
|
|
710
|
-
}
|
|
711
|
-
}, 100);
|
|
712
|
-
});
|
|
713
|
-
return;
|
|
714
|
-
}
|
|
692
|
+
async enableScreenShare() {
|
|
693
|
+
// Guard: do nothing if already sharing or a consent dialog is in flight.
|
|
694
|
+
if (this.screenShareEnabled || this._pendingScreenShare) return;
|
|
715
695
|
if (Platform.OS === 'android') {
|
|
716
696
|
this._pendingScreenShare = true;
|
|
717
|
-
await this.configureForeground();
|
|
718
697
|
try {
|
|
698
|
+
// getScreenShareTracks() manages the full flow:
|
|
699
|
+
// consent dialog → start MediaProjectionService → create MediaStream
|
|
719
700
|
this.setScreenShareTracks(await LocalMediaUtils.getScreenShareTracks());
|
|
720
701
|
this.screenShareEnabled = true;
|
|
721
702
|
} catch (e) {
|
|
722
703
|
console.warn('Failed to start screenshare', e);
|
|
723
704
|
this.screenShareEnabled = false;
|
|
724
|
-
const val = await RTKHelper.isForegroundServiceRunning();
|
|
725
|
-
if (val) {
|
|
726
|
-
await RTKHelper.stopService();
|
|
727
|
-
}
|
|
728
|
-
this._pendingScreenShare = false;
|
|
729
705
|
} finally {
|
|
730
706
|
this._pendingScreenShare = false;
|
|
731
707
|
}
|
|
@@ -750,11 +726,32 @@ export default class LocalMediaHandler extends EventEmitter {
|
|
|
750
726
|
});
|
|
751
727
|
}
|
|
752
728
|
}
|
|
753
|
-
async enableScreenShare() {
|
|
754
|
-
await this.toggleScreenShare();
|
|
755
|
-
}
|
|
756
729
|
async disableScreenShare() {
|
|
757
|
-
|
|
730
|
+
// Guard: nothing to stop if not sharing and no consent dialog is pending.
|
|
731
|
+
if (!this.screenShareEnabled && !this._pendingScreenShare) return;
|
|
732
|
+
if (Platform.OS !== 'ios') {
|
|
733
|
+
this.removeScreenShareTracks();
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// iOS: trigger the broadcast picker to stop the extension.
|
|
738
|
+
NativeModules.RTKScreensharePickerView.showScreenSharePickerView();
|
|
739
|
+
_classPrivateFieldSet(_screenShareState, this, 'PENDING');
|
|
740
|
+
await new Promise((resolve, reject) => {
|
|
741
|
+
let timeoutId;
|
|
742
|
+
let time = 0;
|
|
743
|
+
timeoutId = setInterval(() => {
|
|
744
|
+
if (_classPrivateFieldGet(_screenShareState, this) === 'STOPPED') {
|
|
745
|
+
clearInterval(timeoutId);
|
|
746
|
+
resolve();
|
|
747
|
+
}
|
|
748
|
+
time = time + 100;
|
|
749
|
+
if (time > 20000) {
|
|
750
|
+
clearInterval(timeoutId);
|
|
751
|
+
reject('Timeout exceeded for disableScreenShare()');
|
|
752
|
+
}
|
|
753
|
+
}, 100);
|
|
754
|
+
});
|
|
758
755
|
}
|
|
759
756
|
getAllDevices() {
|
|
760
757
|
return _classPrivateFieldGet(_localMediaUtils, this).availableDevices;
|
|
@@ -881,10 +878,45 @@ export default class LocalMediaHandler extends EventEmitter {
|
|
|
881
878
|
this.emit('DEVICE_LIST_UPDATED', changedDevices);
|
|
882
879
|
}
|
|
883
880
|
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Override the meeting foreground service config before the first meeting
|
|
884
|
+
* session. Called by useRealtimeKitClient when keepAliveService is set.
|
|
885
|
+
*/
|
|
886
|
+
static configure(config) {
|
|
887
|
+
_meetingServiceConfig = {
|
|
888
|
+
..._meetingServiceConfig,
|
|
889
|
+
...config
|
|
890
|
+
};
|
|
891
|
+
}
|
|
884
892
|
static async init(_) {
|
|
885
893
|
const localMediaUtils = await LocalMediaUtils.init();
|
|
886
894
|
return new LocalMediaHandler(localMediaUtils);
|
|
887
895
|
}
|
|
896
|
+
async startMeetingService() {
|
|
897
|
+
if (Platform.OS !== 'android') return;
|
|
898
|
+
if (!_meetingServiceConfig.enabled) return;
|
|
899
|
+
|
|
900
|
+
// Android 13+ (API 33): request POST_NOTIFICATIONS so the foreground-service
|
|
901
|
+
// notification appears in the shade.
|
|
902
|
+
if (Number(Platform.Version) >= 33) {
|
|
903
|
+
try {
|
|
904
|
+
await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS);
|
|
905
|
+
} catch (e) {
|
|
906
|
+
console.warn('Failed to request POST_NOTIFICATIONS permission', e);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
try {
|
|
910
|
+
await RTKHelper.startMeetingService({
|
|
911
|
+
title: _meetingServiceConfig.title,
|
|
912
|
+
text: _meetingServiceConfig.text,
|
|
913
|
+
channelId: _meetingServiceConfig.channelId,
|
|
914
|
+
channelName: _meetingServiceConfig.channelName
|
|
915
|
+
});
|
|
916
|
+
} catch (e) {
|
|
917
|
+
console.warn('Failed to start KeepAliveService', e);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
888
920
|
emit(event, ...args) {
|
|
889
921
|
return super.emit(event, ...args);
|
|
890
922
|
}
|