diff --git a/Example/index.js b/Example/index.js index c763ef68..9a556ed2 100644 --- a/Example/index.js +++ b/Example/index.js @@ -55,10 +55,13 @@ const Example = (props) => { }; const _onShareButtonPressed = () => { - twilioVideo.current.toggleScreenSharing(!isSharing); - setIsSharing(!isSharing); + twilioVideo.current.setScreenShareEnabled(!isScreenShareEnabled); }; + const _onScreenShareChanged = ({screenShareEnabled = false}) => { + setIsScreenShareEnabled(screenShareEnabled); + } + const _onFlipButtonPress = () => { twilioVideo.current.flipCamera(); }; @@ -205,7 +208,7 @@ const Example = (props) => { onPress={_onShareButtonPressed} > - {isSharing ? "Stop Sharing" : "Start Sharing"} + {isScreenShareEnabled ? "Stop Screen Sharing" : "Start Screen Sharing"} @@ -217,6 +220,7 @@ const Example = (props) => { ref={twilioVideo} onRoomDidConnect={_onRoomDidConnect} onRoomDidDisconnect={_onRoomDidDisconnect} + onScreenShareChanged={_onScreenShareChanged} onRoomDidFailToConnect={_onRoomDidFailToConnect} onParticipantAddedVideoTrack={_onParticipantAddedVideoTrack} onParticipantRemovedVideoTrack={_onParticipantRemovedVideoTrack} diff --git a/README.md b/README.md index 6ef2b7c0..bcfd918b 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,7 @@ import { Here you can see a complete example of a simple application that uses almost all the apis: ````javascript -import React, { Component, useRef } from 'react'; +import React, { useState, useRef } from 'react'; import { TwilioVideoLocalView, TwilioVideoParticipantView, @@ -221,6 +221,7 @@ import { const Example = (props) => { const [isAudioEnabled, setIsAudioEnabled] = useState(true); const [isVideoEnabled, setIsVideoEnabled] = useState(true); + const [isScreenShareEnabled, setIsScreenShareEnabled] = useState(false); const [status, setStatus] = useState('disconnected'); const [participants, setParticipants] = useState(new Map()); const [videoTracks, setVideoTracks] = useState(new Map()); @@ -231,7 +232,7 @@ const Example = (props) => { twilioRef.current.connect({ accessToken: token }); setStatus('connecting'); } - + const _onEndButtonPress = () => { twilioRef.current.disconnect(); }; @@ -242,6 +243,14 @@ const Example = (props) => { .then(isEnabled => setIsAudioEnabled(isEnabled)); }; + const _onShareButtonPressed = () => { + twilioRef.current.setScreenShareEnabled(!isScreenShareEnabled); + }; + + const _onScreenShareChanged = ({screenShareEnabled = false}) => { + setIsScreenShareEnabled(screenShareEnabled); + } + const _onFlipButtonPress = () => { twilioRef.current.flipCamera(); }; @@ -310,21 +319,21 @@ const Example = (props) => { } { - (status === 'connected' || status === 'connecting') && - + (status === 'connected' || status === 'connecting') && + { status === 'connected' && { Array.from(videoTracks, ([trackSid, trackIdentifier]) => { - return ( - - ) - }) + return ( + + ) + }) } } @@ -345,10 +354,17 @@ const Example = (props) => { onPress={_onFlipButtonPress}> Flip + + + {isScreenShareEnabled ? "Stop Screen Sharing" : "Start Screen Sharing"} + + + /> } @@ -357,6 +373,7 @@ const Example = (props) => { ref={ twilioRef } onRoomDidConnect={ _onRoomDidConnect } onRoomDidDisconnect={ _onRoomDidDisconnect } + onScreenShareChanged={ _onScreenShareChanged} onRoomDidFailToConnect= { _onRoomDidFailToConnect } onParticipantAddedVideoTrack={ _onParticipantAddedVideoTrack } onParticipantRemovedVideoTrack= { _onParticipantRemovedVideoTrack } diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 86d3dbfe..f6c3265e 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -16,4 +16,13 @@ + + + + + + diff --git a/android/src/main/java/com/twiliorn/library/CustomTwilioVideoView.java b/android/src/main/java/com/twiliorn/library/CustomTwilioVideoView.java index a29c53ab..9e54c79f 100644 --- a/android/src/main/java/com/twiliorn/library/CustomTwilioVideoView.java +++ b/android/src/main/java/com/twiliorn/library/CustomTwilioVideoView.java @@ -12,6 +12,7 @@ import java.util.HashMap; import java.util.Map; +import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -20,6 +21,7 @@ import android.media.AudioDeviceInfo; import android.media.AudioFocusRequest; import android.media.AudioManager; +import android.media.projection.MediaProjectionManager; import android.os.Build; import androidx.annotation.Nullable; import androidx.annotation.NonNull; @@ -29,16 +31,21 @@ import android.util.Log; import android.view.View; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ActivityEventListener; +import com.facebook.react.bridge.BaseActivityEventListener; import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeArray; import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.events.RCTEventEmitter; import com.twilio.video.AudioTrackPublication; import com.twilio.video.BaseTrackStats; import com.twilio.video.CameraCapturer; +import com.twilio.video.ScreenCapturer; import com.twilio.video.ConnectOptions; import com.twilio.video.LocalAudioTrack; import com.twilio.video.LocalAudioTrackPublication; @@ -112,15 +119,18 @@ import static com.twiliorn.library.CustomTwilioVideoView.Events.ON_VIDEO_CHANGED; import static com.twiliorn.library.CustomTwilioVideoView.Events.ON_DOMINANT_SPEAKER_CHANGED; import static com.twiliorn.library.CustomTwilioVideoView.Events.ON_LOCAL_PARTICIPANT_SUPPORTED_CODECS; +import static com.twiliorn.library.CustomTwilioVideoView.Events.ON_SCREEN_SHARE_CHANGED; public class CustomTwilioVideoView extends View implements LifecycleEventListener, AudioManager.OnAudioFocusChangeListener { private static final String TAG = "CustomTwilioVideoView"; private static final String DATA_TRACK_MESSAGE_THREAD_NAME = "DataTrackMessages"; private static final String FRONT_CAMERA_TYPE = "front"; private static final String BACK_CAMERA_TYPE = "back"; + private static final int REQUEST_MEDIA_PROJECTION = 100; private boolean enableRemoteAudio = false; private boolean enableNetworkQualityReporting = false; private boolean isVideoEnabled = false; + private boolean isScreenShareEnabled = false; private boolean dominantSpeakerEnabled = false; private static String frontFacingDevice; private static String backFacingDevice; @@ -152,6 +162,7 @@ public class CustomTwilioVideoView extends View implements LifecycleEventListene Events.ON_NETWORK_QUALITY_LEVELS_CHANGED, Events.ON_DOMINANT_SPEAKER_CHANGED, Events.ON_LOCAL_PARTICIPANT_SUPPORTED_CODECS, + Events.ON_SCREEN_SHARE_CHANGED, }) public @interface Events { String ON_CAMERA_SWITCHED = "onCameraSwitched"; @@ -177,6 +188,7 @@ public class CustomTwilioVideoView extends View implements LifecycleEventListene String ON_NETWORK_QUALITY_LEVELS_CHANGED = "onNetworkQualityLevelsChanged"; String ON_DOMINANT_SPEAKER_CHANGED = "onDominantSpeakerDidChange"; String ON_LOCAL_PARTICIPANT_SUPPORTED_CODECS = "onLocalParticipantSupportedCodecs"; + String ON_SCREEN_SHARE_CHANGED = "onScreenShareChanged"; } private final ThemedReactContext themedReactContext; @@ -202,8 +214,11 @@ public class CustomTwilioVideoView extends View implements LifecycleEventListene private static LocalVideoTrack localVideoTrack; private static CameraCapturer cameraCapturer; + private static ScreenCapturer screenCapturer; + private ScreenCapturerManager screenCapturerManager; private LocalAudioTrack localAudioTrack; private AudioManager audioManager; + private MediaProjectionManager mediaProjectionManager; private int previousAudioMode; private boolean disconnectedFromOnDestroy; private IntentFilter intentFilter; @@ -220,6 +235,34 @@ public class CustomTwilioVideoView extends View implements LifecycleEventListene private final Map dataTrackRemoteParticipantMap = new HashMap<>(); + private final ActivityEventListener activityEventListener = new BaseActivityEventListener() { + @Override + public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { + Log.d("RNTwilioScreenShare", "Got activity result " + requestCode + " " + resultCode); + super.onActivityResult(activity, requestCode, resultCode, data); + if (requestCode == REQUEST_MEDIA_PROJECTION) { + Log.d("RNTwilioScreenShare", "Request for the screen capture permission"); + if (resultCode != Activity.RESULT_OK) { + Log.d("RNTwilioScreenShare", "Screen capture permission not granted"); + } else { + screenCapturer = new ScreenCapturer(themedReactContext, resultCode, data, new ScreenCapturer.Listener() { + @Override + public void onFirstFrameAvailable() { + Log.d("RNTwilioScreenShare", "First frame from screen capturer available"); + } + + @Override + public void onScreenCaptureError(String errorDescription) { + Log.e("RNTwilioScreenShare", "Screen capturer error: " + errorDescription); + stopScreenCapture(); + } + }); + startScreenCapture(); + } + } + } + }; + public CustomTwilioVideoView(ThemedReactContext context) { super(context); this.themedReactContext = context; @@ -243,6 +286,15 @@ public CustomTwilioVideoView(ThemedReactContext context) { dataTrackMessageThread.start(); dataTrackMessageThreadHandler = new Handler(dataTrackMessageThread.getLooper()); + Activity currentActivity = context.getCurrentActivity(); + mediaProjectionManager = (MediaProjectionManager) currentActivity.getApplication().getSystemService(Context.MEDIA_PROJECTION_SERVICE); + + ReactApplicationContext getReactApplicationContext = context.getReactApplicationContext(); + getReactApplicationContext.addActivityEventListener(activityEventListener); + + if (android.os.Build.VERSION.SDK_INT >= 29) { + screenCapturerManager = new ScreenCapturerManager(getContext()); + } } // ===== SETUP ================================================================================= @@ -346,8 +398,12 @@ public void onHostResume() { /* * If the local video track was released when the app was put in the background, recreate. */ - if (cameraCapturer != null && localVideoTrack == null) { - localVideoTrack = LocalVideoTrack.create(getContext(), isVideoEnabled, cameraCapturer, buildVideoFormat()); + if (localVideoTrack == null) { + if(screenCapturer != null) { + localVideoTrack = LocalVideoTrack.create(getContext(), isScreenShareEnabled, screenCapturer); + } else if(cameraCapturer != null) { + localVideoTrack = LocalVideoTrack.create(getContext(), isVideoEnabled, cameraCapturer, buildVideoFormat()); + } } if (localVideoTrack != null) { @@ -417,6 +473,10 @@ public void onHostDestroy() { localVideoTrack = null; } + if (android.os.Build.VERSION.SDK_INT >= 29) { + screenCapturerManager.unbindService(); + } + if (localAudioTrack != null) { localAudioTrack.release(); audioManager.stopBluetoothSco(); @@ -435,6 +495,7 @@ public void releaseResource() { localVideoTrack = null; thumbnailVideoView = null; cameraCapturer = null; + screenCapturer = null; } // ====== CONNECTING =========================================================================== @@ -667,6 +728,10 @@ public void disconnect() { cameraCapturer.stopCapture(); cameraCapturer = null; } + if (screenCapturer != null) { + screenCapturer.stopCapture(); + screenCapturer = null; + } } // ===== SEND STRING ON DATA TRACK ====================================================================== @@ -704,9 +769,22 @@ public void switchCamera() { } public void toggleVideo(boolean enabled) { + if(enabled && screenCapturer != null && localVideoTrack != null) { + localVideoTrack.enable(false); + publishLocalVideo(false); + + localVideoTrack.release(); + localVideoTrack = null; + screenCapturer = null; + + WritableMap event = new WritableNativeMap(); + event.putBoolean("screenShareEnabled", false); + pushEvent(CustomTwilioVideoView.this, ON_SCREEN_SHARE_CHANGED, event); + } + isVideoEnabled = enabled; - if (cameraCapturer == null && enabled) { + if (enabled && cameraCapturer == null) { String fallbackCameraType = cameraType == null ? CustomTwilioVideoView.FRONT_CAMERA_TYPE : cameraType; boolean createVideoStatus = createLocalVideo(true, fallbackCameraType); if (!createVideoStatus) { @@ -715,16 +793,104 @@ public void toggleVideo(boolean enabled) { } } - if (localVideoTrack != null) { + if (cameraCapturer != null && localVideoTrack != null) { localVideoTrack.enable(enabled); publishLocalVideo(enabled); + if(!enabled) { + localVideoTrack.release(); + localVideoTrack = null; + cameraCapturer = null; + } + WritableMap event = new WritableNativeMap(); event.putBoolean("videoEnabled", enabled); pushEvent(CustomTwilioVideoView.this, ON_VIDEO_CHANGED, event); } } + public void toggleScreenShare(boolean enabled) { + if (enabled) { + if (android.os.Build.VERSION.SDK_INT >= 29) { + screenCapturerManager.startForeground(); + } + if(screenCapturer == null) { + // This initiates a prompt dialog for the user to confirm screen projection. + + if (mediaProjectionManager != null) { + Activity currentActivity = this.themedReactContext.getCurrentActivity(); + + UiThreadUtil.runOnUiThread(new Runnable() { + @Override + public void run() { + assert currentActivity != null; + currentActivity.startActivityForResult( + mediaProjectionManager.createScreenCaptureIntent(), REQUEST_MEDIA_PROJECTION); + } + }); + } else { + Log.d("RNTwilioScreenShare", "mediaProjectionManager is null"); + } + } else { + startScreenCapture(); + } + } else { + if (android.os.Build.VERSION.SDK_INT >= 29) { + screenCapturerManager.endForeground(); + } + stopScreenCapture(); + } + } + + private void startScreenCapture() { + if(cameraCapturer != null && localVideoTrack != null){ + localVideoTrack.enable(false); + publishLocalVideo(false); + + localVideoTrack.release(); + localVideoTrack = null; + cameraCapturer = null; + + WritableMap event = new WritableNativeMap(); + event.putBoolean("videoEnabled", false); + pushEvent(CustomTwilioVideoView.this, ON_VIDEO_CHANGED, event); + } + + isScreenShareEnabled = true; + + localVideoTrack = LocalVideoTrack.create(getContext(), true, screenCapturer); + + if (thumbnailVideoView != null && localVideoTrack != null) { + localVideoTrack.addSink(thumbnailVideoView); + } + + if (screenCapturer != null && localVideoTrack != null) { + localVideoTrack.enable(true); + publishLocalVideo(true); + + WritableMap event = new WritableNativeMap(); + event.putBoolean("screenShareEnabled", true); + pushEvent(CustomTwilioVideoView.this, ON_SCREEN_SHARE_CHANGED, event); + } + } + + private void stopScreenCapture() { + isScreenShareEnabled = false; + + if (screenCapturer != null && localVideoTrack != null) { + localVideoTrack.enable(false); + publishLocalVideo(false); + + localVideoTrack.release(); + localVideoTrack = null; + screenCapturer = null; + + WritableMap event = new WritableNativeMap(); + event.putBoolean("screenShareEnabled", false); + pushEvent(CustomTwilioVideoView.this, ON_SCREEN_SHARE_CHANGED, event); + } + } + public void toggleSoundSetup(boolean speaker) { AudioManager audioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE); if (speaker) { diff --git a/android/src/main/java/com/twiliorn/library/CustomTwilioVideoViewManager.java b/android/src/main/java/com/twiliorn/library/CustomTwilioVideoViewManager.java index 3705059f..43472b01 100644 --- a/android/src/main/java/com/twiliorn/library/CustomTwilioVideoViewManager.java +++ b/android/src/main/java/com/twiliorn/library/CustomTwilioVideoViewManager.java @@ -42,6 +42,7 @@ import static com.twiliorn.library.CustomTwilioVideoView.Events.ON_NETWORK_QUALITY_LEVELS_CHANGED; import static com.twiliorn.library.CustomTwilioVideoView.Events.ON_DOMINANT_SPEAKER_CHANGED; import static com.twiliorn.library.CustomTwilioVideoView.Events.ON_LOCAL_PARTICIPANT_SUPPORTED_CODECS; +import static com.twiliorn.library.CustomTwilioVideoView.Events.ON_SCREEN_SHARE_CHANGED; public class CustomTwilioVideoViewManager extends SimpleViewManager { public static final String REACT_CLASS = "RNCustomTwilioVideoView"; @@ -60,6 +61,7 @@ public class CustomTwilioVideoViewManager extends SimpleViewManager 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)