getCommandsMap() {
.put("disconnect", DISCONNECT)
.put("switchCamera", SWITCH_CAMERA)
.put("toggleVideo", TOGGLE_VIDEO)
+ .put("toggleScreenShare", TOGGLE_SCREEN_SHARE)
.put("toggleSound", TOGGLE_SOUND)
.put("getStats", GET_STATS)
.put("disableOpenSLES", DISABLE_OPENSL_ES)
diff --git a/android/src/main/java/com/twiliorn/library/ScreenCapturerManager.java b/android/src/main/java/com/twiliorn/library/ScreenCapturerManager.java
new file mode 100644
index 00000000..80bd9120
--- /dev/null
+++ b/android/src/main/java/com/twiliorn/library/ScreenCapturerManager.java
@@ -0,0 +1,73 @@
+/**
+ * Service to orchestrate the Twilio Screen Share connection and the various video
+ * views.
+ *
+ * Authors:
+ * Manish Sahu
+ */
+package com.twiliorn.library;
+
+import android.annotation.TargetApi;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+
+@TargetApi(29)
+public class ScreenCapturerManager {
+ private ScreenCapturerService mService;
+ private Context mContext;
+ private State currentState = State.UNBIND_SERVICE;
+
+ /** Defines callbacks for service binding, passed to bindService() */
+ private ServiceConnection connection =
+ new ServiceConnection() {
+
+ @Override
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ // We've bound to ScreenCapturerService, cast the IBinder and get
+ // ScreenCapturerService instance
+ ScreenCapturerService.LocalBinder binder =
+ (ScreenCapturerService.LocalBinder) service;
+ mService = binder.getService();
+ currentState = State.BIND_SERVICE;
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName arg0) {}
+ };
+
+ /** An enum describing the possible states of a ScreenCapturerManager. */
+ public enum State {
+ BIND_SERVICE,
+ START_FOREGROUND,
+ END_FOREGROUND,
+ UNBIND_SERVICE
+ }
+
+ ScreenCapturerManager(Context context) {
+ mContext = context;
+ bindService();
+ }
+
+ private void bindService() {
+ Intent intent = new Intent(mContext, ScreenCapturerService.class);
+ mContext.bindService(intent, connection, Context.BIND_AUTO_CREATE);
+ }
+
+ void startForeground() {
+ mService.startForeground();
+ currentState = State.START_FOREGROUND;
+ }
+
+ void endForeground() {
+ mService.endForeground();
+ currentState = State.END_FOREGROUND;
+ }
+
+ void unbindService() {
+ mContext.unbindService(connection);
+ currentState = State.UNBIND_SERVICE;
+ }
+}
diff --git a/android/src/main/java/com/twiliorn/library/ScreenCapturerService.java b/android/src/main/java/com/twiliorn/library/ScreenCapturerService.java
new file mode 100644
index 00000000..03cd7721
--- /dev/null
+++ b/android/src/main/java/com/twiliorn/library/ScreenCapturerService.java
@@ -0,0 +1,81 @@
+/**
+ * Service to orchestrate the Twilio Screen Share connection and the various video
+ * views.
+ *
+ * Authors:
+ * Manish Sahu
+ */
+package com.twiliorn.library;
+
+import android.annotation.TargetApi;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+import android.support.v4.app.NotificationCompat;
+
+@TargetApi(29)
+public class ScreenCapturerService extends Service {
+ private static final String CHANNEL_ID = "screen_capture";
+ private static final String CHANNEL_NAME = "Screen_Capture";
+
+ // Binder given to clients
+ private final IBinder binder = new LocalBinder();
+
+ /**
+ * Class used for the client Binder. We know this service always runs in the same process as its
+ * clients, we don't need to deal with IPC.
+ */
+ public class LocalBinder extends Binder {
+ public ScreenCapturerService getService() {
+ // Return this instance of ScreenCapturerService so clients can call public methods
+ return ScreenCapturerService.this;
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ return START_NOT_STICKY;
+ }
+
+ public void startForeground() {
+ NotificationChannel chan =
+ new NotificationChannel(
+ CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_NONE);
+ NotificationManager manager =
+ (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ assert manager != null;
+ manager.createNotificationChannel(chan);
+
+ final int notificationId = (int) System.currentTimeMillis();
+ NotificationCompat.Builder notificationBuilder =
+ new NotificationCompat.Builder(this, CHANNEL_ID);
+ Notification notification =
+ notificationBuilder
+ .setOngoing(true)
+ // .setSmallIcon(R.drawable.ic_screen_share_white_24dp)
+ .setContentTitle("ScreenCapturerService is running in the foreground")
+ .setPriority(NotificationManager.IMPORTANCE_MIN)
+ .setCategory(Notification.CATEGORY_SERVICE)
+ .build();
+ startForeground(notificationId, notification);
+ }
+
+ public void endForeground() {
+ stopForeground(true);
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return binder;
+ }
+}
diff --git a/docs/README.md b/docs/README.md
index cc2631b9..c578d95d 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -14,6 +14,7 @@ Property | Type | Required | Default value | Description
:--- | :--- | :--- | :--- | :---
onCameraSwitched | func | no | | Callback that is called when camera source changes
onVideoChanged | func | no | | Callback that is called when video is toggled.
+onScreenShareChanged | func | no | | Callback that is called when screen share is toggled.
onAudioChanged | func | no | | Callback that is called when a audio is toggled.
onRoomDidConnect | func | no | | Called when the room has connected @param {{roomName, participants, localParticipant}}
onRoomDidFailToConnect | func | no | | Callback that is called when connecting to room fails.
@@ -46,7 +47,7 @@ onDominantSpeakerDidChange | func | no | | Called when dominant speaker changes
Property | Type | Required | Default value | Description
:--- | :--- | :--- | :--- | :---
-screenShare | bool | no | | Flag that enables screen sharing RCTRootView instead of camera capture
+onScreenShareChanged | func | no | | Callback that is called when screen share is toggled.
onRoomDidConnect | func | no | | Called when the room has connected @param {{roomName, participants, localParticipant}}
onRoomDidDisconnect | func | no | | Called when the room has disconnected @param {{roomName, error}}
onRoomDidFailToConnect | func | no | | Called when connection with room failed @param {{roomName, error}}
diff --git a/index.d.ts b/index.d.ts
index c8410162..f09f10bf 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -104,6 +104,7 @@ declare module "react-native-twilio-video-webrtc" {
onCameraDidStart?: () => void;
onCameraDidStopRunning?: (err: any) => void;
onCameraWasInterrupted?: () => void;
+ onScreenShareChanged?: (data: any) => void;
onDominantSpeakerDidChange?: DominantSpeakerChangedCb;
onParticipantAddedAudioTrack?: TrackEventCb;
onParticipantAddedVideoTrack?: TrackEventCb;
@@ -163,6 +164,7 @@ declare module "react-native-twilio-video-webrtc" {
class TwilioVideo extends React.Component {
setLocalVideoEnabled: (enabled: boolean) => Promise;
+ setScreenShareEnabled: (enabled: boolean) => void;
setLocalAudioEnabled: (enabled: boolean) => Promise;
setRemoteAudioEnabled: (enabled: boolean) => Promise;
setBluetoothHeadsetConnected: (enabled: boolean) => Promise;
diff --git a/ios/RCTTWVideoModule.m b/ios/RCTTWVideoModule.m
index 2311f6c5..0e1eabe7 100644
--- a/ios/RCTTWVideoModule.m
+++ b/ios/RCTTWVideoModule.m
@@ -10,6 +10,7 @@
#import "RCTTWSerializable.h"
+static NSString* screenShareChanged = @"screenShareChanged";
static NSString* roomDidConnect = @"roomDidConnect";
static NSString* roomDidDisconnect = @"roomDidDisconnect";
static NSString* roomDidFailToConnect = @"roomDidFailToConnect";
@@ -82,6 +83,7 @@ @implementation RCTTWVideoModule
- (void)dealloc {
[self clearCameraInstance];
+ [self clearScreenInstance];
}
- (dispatch_queue_t)methodQueue {
@@ -90,6 +92,7 @@ - (dispatch_queue_t)methodQueue {
- (NSArray *)supportedEvents {
return @[
+ screenShareChanged,
roomDidConnect,
roomDidDisconnect,
roomDidFailToConnect,
@@ -194,6 +197,7 @@ - (void)startCameraCapture:(NSString *)cameraType {
for (TVIVideoView *renderer in self.localVideoTrack.renderers) {
[self updateLocalViewMirroring:renderer];
}
+ NSLog(@"NSLog -------- Camera enabled -------- ");
[self sendEventCheckingListenerWithName:cameraDidStart body:nil];
}
}];
@@ -249,16 +253,50 @@ - (bool)_setLocalVideoEnabled:(bool)enabled {
}
- (bool)_setLocalVideoEnabled:(bool)enabled cameraType:(NSString *)cameraType {
- if (self.localVideoTrack != nil) {
+ if(enabled && self.screen != nil && self.localVideoTrack != nil) {
+ [self.localVideoTrack setEnabled:!enabled];
+ TVILocalParticipant *localParticipant = self.room.localParticipant;
+ [localParticipant unpublishVideoTrack:self.localVideoTrack];
+
+ [self.screen stopCaptureWithCompletion:^(NSError * _Nullable error) {
+ if(!error) {
+ NSLog(@"NSLog -------- Screen share disabled -------- ");
+ [self sendEventCheckingListenerWithName:screenShareChanged body:@{ @"screenShareEnabled": [NSNumber numberWithBool:false] }];
+ }
+ }];
+
+ self.localVideoTrack = nil;
+ self.screen = nil;
+ }
+
+ if(enabled && self.camera == nil) {
+ TVICameraSourceOptions *options = [TVICameraSourceOptions optionsWithBlock:^(TVICameraSourceOptionsBuilder * _Nonnull builder) {
+
+ }];
+ self.camera = [[TVICameraSource alloc] initWithOptions:options delegate:self];
+ if (self.camera == nil) {
+ return false;
+ }
+ self.localVideoTrack = [TVILocalVideoTrack trackWithSource:self.camera enabled:NO name:@"camera"];
+ }
+
+ if (self.camera != nil && self.localVideoTrack != nil) {
+ if (enabled) {
[self.localVideoTrack setEnabled:enabled];
- if (self.camera) {
- if (enabled) {
- [self startCameraCapture:cameraType];
- } else {
- [self clearCameraInstance];
- }
- return enabled;
- }
+ TVILocalParticipant *localParticipant = self.room.localParticipant;
+ [localParticipant publishVideoTrack:self.localVideoTrack];
+
+ [self startCameraCapture:cameraType];
+ } else {
+ [self.localVideoTrack setEnabled:enabled];
+ TVILocalParticipant *localParticipant = self.room.localParticipant;
+ [localParticipant unpublishVideoTrack:self.localVideoTrack];
+
+ [self.camera stopCapture];
+ self.localVideoTrack = nil;
+ self.camera = nil;
+ }
+ return enabled;
}
return false;
}
@@ -288,26 +326,60 @@ - (bool)_setLocalVideoEnabled:(bool)enabled cameraType:(NSString *)cameraType {
}
}
-RCT_EXPORT_METHOD(toggleScreenSharing: (BOOL) value) {
- if (value) {
- TVIAppScreenSourceOptions *options = [TVIAppScreenSourceOptions optionsWithBlock:^(TVIAppScreenSourceOptionsBuilder * _Nonnull builder) {
+RCT_EXPORT_METHOD(toggleScreenShare:(BOOL)enabled) {
+ if (enabled) {
+ if(self.camera != nil && self.localVideoTrack != nil) {
+ [self.localVideoTrack setEnabled:!enabled];
+ TVILocalParticipant *localParticipant = self.room.localParticipant;
+ [localParticipant unpublishVideoTrack:self.localVideoTrack];
- }];
- self.screen = [[TVIAppScreenSource alloc] initWithOptions:options delegate:self];
- if (self.screen == nil) {
- return;
- }
- self.localVideoTrack = [TVILocalVideoTrack trackWithSource:self.screen enabled:YES name:@"screen"];
- if(self.localVideoTrack != nil){
- TVILocalParticipant *localParticipant = self.room.localParticipant;
- [localParticipant publishVideoTrack:self.localVideoTrack];
- }
- [self.screen startCapture];
+ [self.camera stopCaptureWithCompletion:^(NSError * _Nullable error) {
+ if(!error) {
+ NSLog(@"NSLog -------- Camera disabled -------- ");
+ [self sendEventCheckingListenerWithName:cameraDidStopRunning body:nil];
+ }
+ }];
+
+ self.localVideoTrack = nil;
+ self.camera = nil;
+ }
+
+ if(self.screen == nil) {
+ TVIAppScreenSourceOptions *options = [TVIAppScreenSourceOptions optionsWithBlock:^(TVIAppScreenSourceOptionsBuilder * _Nonnull builder) {
+
+ }];
+ self.screen = [[TVIAppScreenSource alloc] initWithOptions:options delegate:self];
+ if (self.screen == nil) {
+ return;
+ }
+ self.localVideoTrack = [TVILocalVideoTrack trackWithSource:self.screen enabled:NO name:@"screen"];
+ }
+
+ if(self.screen != nil && self.localVideoTrack != nil){
+ [self.localVideoTrack setEnabled:enabled];
+ TVILocalParticipant *localParticipant = self.room.localParticipant;
+ [localParticipant publishVideoTrack:self.localVideoTrack];
+
+ [self.screen startCaptureWithCompletion:^(NSError * _Nullable error) {
+ if (!error) {
+ NSLog(@"NSLog -------- Screen share enabled -------- ");
+ [self sendEventCheckingListenerWithName:screenShareChanged body:@{ @"screenShareEnabled": [NSNumber numberWithBool:true] }];
+ }
+ }];
+ }
} else {
- [self unpublishLocalVideo];
- [self.screen stopCapture];
- self.localVideoTrack = nil;
- }
+ if(self.screen != nil && self.localVideoTrack != nil) {
+ [self.localVideoTrack setEnabled:enabled];
+ TVILocalParticipant *localParticipant = self.room.localParticipant;
+ [localParticipant unpublishVideoTrack:self.localVideoTrack];
+
+ [self.screen stopCapture];
+ self.localVideoTrack = nil;
+ self.screen = nil;
+
+ [self sendEventCheckingListenerWithName:screenShareChanged body:@{ @"screenShareEnabled": [NSNumber numberWithBool:false] }];
+ }
+ }
}
@@ -474,6 +546,7 @@ -(NSMutableDictionary*)convertLocalVideoTrackStats:(TVILocalVideoTrackStats *)st
RCT_EXPORT_METHOD(disconnect) {
[self clearCameraInstance];
+ [self clearScreenInstance];
[self.room disconnect];
}
@@ -481,6 +554,15 @@ - (void)clearCameraInstance {
// We are done with camera
if (self.camera) {
[self.camera stopCapture];
+ self.camera = nil;
+ }
+}
+
+- (void)clearScreenInstance {
+ // We are done with camera
+ if (self.screen) {
+ [self.screen stopCapture];
+ self.screen = nil;
}
}
diff --git a/src/TwilioVideo.android.js b/src/TwilioVideo.android.js
index 0b563e5b..ef6bfcd7 100644
--- a/src/TwilioVideo.android.js
+++ b/src/TwilioVideo.android.js
@@ -30,6 +30,11 @@ const propTypes = {
*/
onVideoChanged: PropTypes.func,
+ /**
+ * Callback that is called when screen share permission received.
+ */
+ onScreenShareChanged: PropTypes.func,
+
/**
* Callback that is called when a audio is toggled.
*/
@@ -165,7 +170,8 @@ const nativeEvents = {
toggleBluetoothHeadset: 11,
sendString: 12,
publishVideo: 13,
- publishAudio: 14
+ publishAudio: 14,
+ toggleScreenShare: 15
}
class CustomTwilioVideoView extends Component {
@@ -234,6 +240,10 @@ class CustomTwilioVideoView extends Component {
return Promise.resolve(enabled)
}
+ setScreenShareEnabled (enabled) {
+ this.runCommand(nativeEvents.toggleScreenShare, [enabled])
+ }
+
setLocalAudioEnabled (enabled) {
this.runCommand(nativeEvents.toggleSound, [enabled])
return Promise.resolve(enabled)
@@ -279,6 +289,7 @@ class CustomTwilioVideoView extends Component {
return [
'onCameraSwitched',
'onVideoChanged',
+ 'onScreenShareChanged',
'onAudioChanged',
'onRoomDidConnect',
'onRoomDidFailToConnect',
diff --git a/src/TwilioVideo.ios.js b/src/TwilioVideo.ios.js
index b9f149fd..a8a1fa78 100644
--- a/src/TwilioVideo.ios.js
+++ b/src/TwilioVideo.ios.js
@@ -14,6 +14,10 @@ const { TWVideoModule } = NativeModules
export default class TwilioVideo extends Component {
static propTypes = {
+ /**
+ * Callback that is called when screen share permission received.
+ */
+ onScreenShareChanged: PropTypes.func,
/**
* Called when the room has connected
*
@@ -215,8 +219,8 @@ export default class TwilioVideo extends Component {
/**
* Toggle screen sharing
*/
- toggleScreenSharing (status) {
- TWVideoModule.toggleScreenSharing(status)
+ setScreenShareEnabled (enabled) {
+ TWVideoModule.toggleScreenShare(enabled)
}
/**
@@ -330,6 +334,11 @@ export default class TwilioVideo extends Component {
_registerEvents () {
TWVideoModule.changeListenerStatus(true)
this._subscriptions = [
+ this._eventEmitter.addListener('screenShareChanged', (data) => {
+ if (this.props.onScreenShareChanged) {
+ this.props.onScreenShareChanged(data)
+ }
+ }),
this._eventEmitter.addListener('roomDidConnect', (data) => {
if (this.props.onRoomDidConnect) {
this.props.onRoomDidConnect(data)