diff --git a/CHANGELOG.md b/CHANGELOG.md index ce407c20..56b6decb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ + +# 0.12.0 (Aug 5, 2019) + +- **[Feature]**: Add Multi-session support (merged from [PR311](https://github.com/opentok/opentok-react-native/pull/311)). Adheres to: [#218](https://github.com/opentok/opentok-react-native/issues/218), [#271](https://github.com/opentok/opentok-react-native/issues/271) + + # 0.11.2 (July 2, 2019) - **[Feature]**: Enable `OTSubscriber` children custom render (merged from [PR306](https://github.com/opentok/opentok-react-native/pull/306)). Adheres to: [#289](https://github.com/opentok/opentok-react-native/issues/289), [#174](https://github.com/opentok/opentok-react-native/issues/174) diff --git a/android/src/main/java/com/opentokreactnative/OTPublisherLayout.java b/android/src/main/java/com/opentokreactnative/OTPublisherLayout.java index bde9c812..438a1fb2 100644 --- a/android/src/main/java/com/opentokreactnative/OTPublisherLayout.java +++ b/android/src/main/java/com/opentokreactnative/OTPublisherLayout.java @@ -27,10 +27,18 @@ public OTPublisherLayout(ThemedReactContext reactContext) { public void createPublisherView(String publisherId) { ConcurrentHashMap mPublishers = sharedState.getPublishers(); - String pubOrSub = sharedState.getAndroidOnTop(); - String zOrder = sharedState.getAndroidZOrder(); + ConcurrentHashMap androidOnTopMap = sharedState.getAndroidOnTopMap(); + ConcurrentHashMap androidZOrderMap = sharedState.getAndroidZOrderMap(); + String pubOrSub = ""; + String zOrder = ""; Publisher mPublisher = mPublishers.get(publisherId); if (mPublisher != null) { + if (androidOnTopMap.get(mPublisher.getSession().getSessionId()) != null) { + pubOrSub = androidOnTopMap.get(mPublisher.getSession().getSessionId()); + } + if (androidZOrderMap.get(mPublisher.getSession().getSessionId()) != null) { + zOrder = androidZOrderMap.get(mPublisher.getSession().getSessionId()); + } mPublisher.setStyle(BaseVideoRenderer.STYLE_VIDEO_SCALE, BaseVideoRenderer.STYLE_VIDEO_FILL); FrameLayout mPublisherViewContainer = new FrameLayout(getContext()); diff --git a/android/src/main/java/com/opentokreactnative/OTRN.java b/android/src/main/java/com/opentokreactnative/OTRN.java index 1804553e..d62b9304 100644 --- a/android/src/main/java/com/opentokreactnative/OTRN.java +++ b/android/src/main/java/com/opentokreactnative/OTRN.java @@ -18,16 +18,18 @@ public class OTRN { public static OTRN sharedState; - private Session mSession; - private String mAndroidOnTop; - private String mAndroidZOrder; private ConcurrentHashMap subscriberStreams = new ConcurrentHashMap<>(); private ConcurrentHashMap subscribers = new ConcurrentHashMap<>(); private ConcurrentHashMap publishers = new ConcurrentHashMap<>(); + private ConcurrentHashMap sessions = new ConcurrentHashMap<>(); + private ConcurrentHashMap androidOnTopMap = new ConcurrentHashMap<>(); + private ConcurrentHashMap androidZOrderMap = new ConcurrentHashMap<>(); private ConcurrentHashMap subscriberViewContainers = new ConcurrentHashMap<>(); private ConcurrentHashMap publisherViewContainers = new ConcurrentHashMap<>(); private ConcurrentHashMap publisherDestroyedCallbacks = new ConcurrentHashMap<>(); + private ConcurrentHashMap sessionConnectCallbacks = new ConcurrentHashMap<>(); + private ConcurrentHashMap sessionDisconnectCallbacks = new ConcurrentHashMap<>(); private ConcurrentHashMap connections = new ConcurrentHashMap<>(); public static synchronized OTRN getSharedState() { @@ -38,37 +40,16 @@ public static synchronized OTRN getSharedState() { return sharedState; } - public synchronized Session getSession() { + public ConcurrentHashMap getAndroidOnTopMap() { - return this.mSession; + return this.androidOnTopMap; } - public synchronized void setSession(Session mSession) { + public ConcurrentHashMap getAndroidZOrderMap() { - this.mSession = mSession; + return this.androidZOrderMap; } - public synchronized String getAndroidOnTop() { - - return this.mAndroidOnTop; - } - - public synchronized void setAndroidOnTop(String androidOnTop) { - - this.mAndroidOnTop = androidOnTop; - } - - public synchronized String getAndroidZOrder() { - - return this.mAndroidZOrder; - } - - public synchronized void setAndroidZOrder(String androidZOrder) { - - this.mAndroidZOrder = androidZOrder; - } - - public ConcurrentHashMap getSubscriberStreams() { return this.subscriberStreams; @@ -99,10 +80,25 @@ public ConcurrentHashMap getPublisherDestroyedCallbacks() { return this.publisherDestroyedCallbacks; } + public ConcurrentHashMap getSessionConnectCallbacks() { + + return this.sessionConnectCallbacks; + } + + public ConcurrentHashMap getSessionDisconnectCallbacks() { + + return this.sessionDisconnectCallbacks; + } + public ConcurrentHashMap getConnections() { return this.connections; } + public ConcurrentHashMap getSessions() { + + return this.sessions; + } + private OTRN() {} } diff --git a/android/src/main/java/com/opentokreactnative/OTSessionManager.java b/android/src/main/java/com/opentokreactnative/OTSessionManager.java index e8f345be..daa3e9a2 100644 --- a/android/src/main/java/com/opentokreactnative/OTSessionManager.java +++ b/android/src/main/java/com/opentokreactnative/OTSessionManager.java @@ -32,7 +32,6 @@ import com.opentokreactnative.utils.EventUtils; import com.opentokreactnative.utils.Utils; -import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.ArrayList; @@ -52,9 +51,7 @@ public class OTSessionManager extends ReactContextBaseJavaModule SubscriberKit.VideoListener, SubscriberKit.StreamListener{ - private Callback connectCallback; - private Callback disconnectCallback; - private int connectionStatus = 0; + private ConcurrentHashMap connectionStatusMap = new ConcurrentHashMap<>(); private ArrayList jsEvents = new ArrayList(); private ArrayList componentEvents = new ArrayList(); private static final String TAG = "OTRN"; @@ -68,7 +65,6 @@ public OTSessionManager(ReactApplicationContext reactContext) { super(reactContext); sharedState = OTRN.getSharedState(); - } @ReactMethod @@ -79,6 +75,10 @@ public void initSession(String apiKey, String sessionId, ReadableMap sessionOpti final boolean connectionEventsSuppressed = sessionOptions.getBoolean("connectionEventsSuppressed"); String androidOnTop = sessionOptions.getString("androidOnTop"); String androidZOrder = sessionOptions.getString("androidZOrder"); + ConcurrentHashMap mSessions = sharedState.getSessions(); + ConcurrentHashMap mAndroidOnTopMap = sharedState.getAndroidOnTopMap(); + ConcurrentHashMap mAndroidZOrderMap = sharedState.getAndroidZOrderMap(); + Session mSession = new Session.Builder(this.getReactApplicationContext(), apiKey, sessionId) .sessionOptions(new Session.SessionOptions() { @@ -100,18 +100,22 @@ public boolean isCamera2Capable() { mSession.setReconnectionListener(this); mSession.setArchiveListener(this); mSession.setStreamPropertiesListener(this); - sharedState.setAndroidOnTop(androidOnTop); - sharedState.setAndroidZOrder(androidZOrder); - sharedState.setSession(mSession); + mSessions.put(sessionId, mSession); + mAndroidOnTopMap.put(sessionId, androidOnTop); + mAndroidZOrderMap.put(sessionId, androidZOrder); } @ReactMethod - public void connect(String token, Callback callback) { - - connectCallback = callback; - Session mSession = sharedState.getSession(); + public void connect(String sessionId, String token, Callback callback) { + ConcurrentHashMap mSessions = sharedState.getSessions(); + ConcurrentHashMap mSessionConnectCallbacks = sharedState.getSessionConnectCallbacks(); + mSessionConnectCallbacks.put(sessionId, callback); + Session mSession = mSessions.get(sessionId); if (mSession != null) { mSession.connect(token); + } else { + WritableMap errorInfo = EventUtils.createError("Error connecting to session. Could not find native session instance"); + callback.invoke(errorInfo); } } @@ -167,19 +171,23 @@ public void initPublisher(String publisherId, ReadableMap properties, Callback c } @ReactMethod - public void publish(String publisherId, Callback callback) { - - Session mSession = sharedState.getSession(); - ConcurrentHashMap mPublishers = sharedState.getPublishers(); - Publisher mPublisher = mPublishers.get(publisherId); - if (mSession != null && mPublisher != null) { - mSession.publish(mPublisher); - callback.invoke(); + public void publish(String sessionId, String publisherId, Callback callback) { + ConcurrentHashMap mSessions = sharedState.getSessions(); + Session mSession = mSessions.get(sessionId); + if (mSession != null) { + ConcurrentHashMap mPublishers = sharedState.getPublishers(); + Publisher mPublisher = mPublishers.get(publisherId); + if (mPublisher != null) { + mSession.publish(mPublisher); + callback.invoke(); + } else { + WritableMap errorInfo = EventUtils.createError("Error publishing. Could not find native publisher instance."); + callback.invoke(errorInfo); + } } else { - WritableMap errorInfo = EventUtils.createError("Error publishing. Could not find native publisher instance."); + WritableMap errorInfo = EventUtils.createError("Error publishing. Could not find native session instance."); callback.invoke(errorInfo); } - } @ReactMethod @@ -187,8 +195,9 @@ public void subscribeToStream(String streamId, ReadableMap properties, Callback ConcurrentHashMap mSubscriberStreams = sharedState.getSubscriberStreams(); ConcurrentHashMap mSubscribers = sharedState.getSubscribers(); - Session mSession = sharedState.getSession(); + ConcurrentHashMap mSessions = sharedState.getSessions(); Stream stream = mSubscriberStreams.get(streamId); + Session mSession = mSessions.get(stream.getSession().getSessionId()); Subscriber mSubscriber = new Subscriber.Builder(getReactApplicationContext(), stream).build(); mSubscriber.setSubscriberListener(this); mSubscriber.setAudioLevelListener(this); @@ -206,7 +215,6 @@ public void subscribeToStream(String streamId, ReadableMap properties, Callback WritableMap errorInfo = EventUtils.createError("Error subscribing. The native session instance could not be found."); callback.invoke(errorInfo); } - } @ReactMethod @@ -239,14 +247,14 @@ public void run() { } @ReactMethod - public void disconnectSession(Callback callback) { - - Session mSession = sharedState.getSession(); - disconnectCallback = callback; + public void disconnectSession(String sessionId, Callback callback) { + ConcurrentHashMap mSessions = sharedState.getSessions(); + ConcurrentHashMap mSessionDisconnectCallbacks = sharedState.getSessionDisconnectCallbacks(); + Session mSession = mSessions.get(sessionId); + mSessionDisconnectCallbacks.put(sessionId, callback); if (mSession != null) { mSession.disconnect(); } - sharedState.setSession(null); } @ReactMethod @@ -332,9 +340,9 @@ public void removeJSComponentEvents(ReadableArray events) { } @ReactMethod - public void sendSignal(ReadableMap signal, Callback callback) { - - Session mSession = sharedState.getSession(); + public void sendSignal(String sessionId, ReadableMap signal, Callback callback) { + ConcurrentHashMap mSessions = sharedState.getSessions(); + Session mSession = mSessions.get(sessionId); ConcurrentHashMap mConnections = sharedState.getConnections(); String connectionId = signal.getString("to"); Connection mConnection = null; @@ -362,12 +370,17 @@ public void destroyPublisher(final String publisherId, final Callback callback) public void run() { ConcurrentHashMap mPublisherDestroyedCallbacks = sharedState.getPublisherDestroyedCallbacks(); - mPublisherDestroyedCallbacks.put(publisherId, callback); ConcurrentHashMap mPublishers = sharedState.getPublishers(); - Publisher mPublisher = mPublishers.get(publisherId); ConcurrentHashMap mPublisherViewContainers = sharedState.getPublisherViewContainers(); + ConcurrentHashMap mSessions = sharedState.getSessions(); FrameLayout mPublisherViewContainer = mPublisherViewContainers.get(publisherId); - Session mSession = sharedState.getSession(); + Publisher mPublisher = mPublishers.get(publisherId); + Session mSession = null; + mPublisherDestroyedCallbacks.put(publisherId, callback); + if (mPublisher != null && mPublisher.getSession() != null) { + mSession = mSessions.get(mPublisher.getSession().getSessionId()); + } + if (mPublisherViewContainer != null) { mPublisherViewContainer.removeAllViews(); } @@ -385,14 +398,14 @@ public void run() { } @ReactMethod - public void getSessionInfo(Callback callback) { - - Session mSession = sharedState.getSession(); + public void getSessionInfo(String sessionId, Callback callback) { + ConcurrentHashMap mSessions = sharedState.getSessions(); + Session mSession = mSessions.get(sessionId); WritableMap sessionInfo = null; if (mSession != null){ sessionInfo = EventUtils.prepareJSSessionMap(mSession); sessionInfo.putString("sessionId", mSession.getSessionId()); - sessionInfo.putInt("connectionStatus", getConnectionStatus()); + sessionInfo.putInt("connectionStatus", getConnectionStatus(mSession.getSessionId())); } callback.invoke(sessionInfo); } @@ -424,14 +437,16 @@ private void sendEventWithString(ReactContext reactContext, String eventName, St } } - private int getConnectionStatus() { - - return this.connectionStatus; + private Integer getConnectionStatus(String sessionId) { + Integer connectionStatus = 0; + if (this.connectionStatusMap.get(sessionId) != null) { + connectionStatus = this.connectionStatusMap.get(sessionId); + } + return connectionStatus; } - private void setConnectionStatus(int connectionStatus) { - - this.connectionStatus = connectionStatus; + private void setConnectionStatus(String sessionId, Integer connectionStatus) { + this.connectionStatusMap.put(sessionId, connectionStatus); } @@ -451,23 +466,28 @@ public String getName() { public void onError(Session session, OpentokError opentokError) { if (Utils.didConnectionFail(opentokError)) { - setConnectionStatus(6); + setConnectionStatus(session.getSessionId(), 6); } WritableMap errorInfo = EventUtils.prepareJSErrorMap(opentokError); - sendEventMap(this.getReactApplicationContext(), sessionPreface + "onError", errorInfo); + sendEventMap(this.getReactApplicationContext(), session.getSessionId() + ":" + sessionPreface + "onError", errorInfo); printLogs("There was an error"); } @Override public void onDisconnected(Session session) { - - setConnectionStatus(0); + ConcurrentHashMap mSessions = sharedState.getSessions(); + ConcurrentHashMap mSessionDisconnectCallbacks = sharedState.getSessionDisconnectCallbacks(); + ConcurrentHashMap mSessionConnectCallbacks = sharedState.getSessionDisconnectCallbacks(); + setConnectionStatus(session.getSessionId(), 0); WritableMap sessionInfo = EventUtils.prepareJSSessionMap(session); - sendEventMap(this.getReactApplicationContext(), sessionPreface + "onDisconnected", sessionInfo); + sendEventMap(this.getReactApplicationContext(), session.getSessionId() + ":" + sessionPreface + "onDisconnected", sessionInfo); + Callback disconnectCallback = mSessionDisconnectCallbacks.get(session.getSessionId()); if (disconnectCallback != null) { disconnectCallback.invoke(); } - disconnectCallback = null; + mSessions.remove(session.getSessionId()); + mSessionConnectCallbacks.remove(session.getSessionId()); + mSessionDisconnectCallbacks.remove(session.getSessionId()); printLogs("onDisconnected: Disconnected from session: " + session.getSessionId()); } @@ -477,7 +497,7 @@ public void onStreamReceived(Session session, Stream stream) { ConcurrentHashMap mSubscriberStreams = sharedState.getSubscriberStreams(); mSubscriberStreams.put(stream.getStreamId(), stream); WritableMap streamInfo = EventUtils.prepareJSStreamMap(stream); - sendEventMap(this.getReactApplicationContext(), sessionPreface + "onStreamReceived", streamInfo); + sendEventMap(this.getReactApplicationContext(), session.getSessionId() + ":" + sessionPreface + "onStreamReceived", streamInfo); printLogs("onStreamReceived: New Stream Received " + stream.getStreamId() + " in session: " + session.getSessionId()); } @@ -485,26 +505,29 @@ public void onStreamReceived(Session session, Stream stream) { @Override public void onConnected(Session session) { - setConnectionStatus(1); - connectCallback.invoke(); + setConnectionStatus(session.getSessionId(), 1); + ConcurrentHashMap mSessionConnectCallbacks = sharedState.getSessionConnectCallbacks(); + Callback mCallback = mSessionConnectCallbacks.get(session.getSessionId()); + if (mCallback != null) { + mCallback.invoke(); + } WritableMap sessionInfo = EventUtils.prepareJSSessionMap(session); - sendEventMap(this.getReactApplicationContext(), sessionPreface + "onConnected", sessionInfo); - connectCallback = null; + sendEventMap(this.getReactApplicationContext(), session.getSessionId() + ":" + sessionPreface + "onConnected", sessionInfo); printLogs("onConnected: Connected to session: "+session.getSessionId()); } @Override public void onReconnected(Session session) { - sendEventMap(this.getReactApplicationContext(), sessionPreface + "onReconnected", null); + sendEventMap(this.getReactApplicationContext(), session.getSessionId() + ":" + sessionPreface + "onReconnected", null); printLogs("Reconnected"); } @Override public void onReconnecting(Session session) { - setConnectionStatus(3); - sendEventMap(this.getReactApplicationContext(), sessionPreface + "onReconnecting", null); + setConnectionStatus(session.getSessionId(), 3); + sendEventMap(this.getReactApplicationContext(), session.getSessionId() + ":" + sessionPreface + "onReconnecting", null); printLogs("Reconnecting"); } @@ -514,7 +537,8 @@ public void onArchiveStarted(Session session, String id, String name) { WritableMap archiveInfo = Arguments.createMap(); archiveInfo.putString("archiveId", id); archiveInfo.putString("name", name); - sendEventMap(this.getReactApplicationContext(), sessionPreface + "onArchiveStarted", archiveInfo); + archiveInfo.putString("sessionId", session.getSessionId()); + sendEventMap(this.getReactApplicationContext(), session.getSessionId() + ":" + sessionPreface + "onArchiveStarted", archiveInfo); printLogs("Archive Started: " + id); } @@ -524,7 +548,8 @@ public void onArchiveStopped(Session session, String id) { WritableMap archiveInfo = Arguments.createMap(); archiveInfo.putString("archiveId", id); archiveInfo.putString("name", ""); - sendEventMap(this.getReactApplicationContext(), sessionPreface + "onArchiveStopped", archiveInfo); + archiveInfo.putString("sessionId", session.getSessionId()); + sendEventMap(this.getReactApplicationContext(), session.getSessionId() + ":" + sessionPreface + "onArchiveStopped", archiveInfo); printLogs("Archive Stopped: " + id); } @Override @@ -533,7 +558,8 @@ public void onConnectionCreated(Session session, Connection connection) { ConcurrentHashMap mConnections = sharedState.getConnections(); mConnections.put(connection.getConnectionId(), connection); WritableMap connectionInfo = EventUtils.prepareJSConnectionMap(connection); - sendEventMap(this.getReactApplicationContext(), sessionPreface + "onConnectionCreated", connectionInfo); + connectionInfo.putString("sessionId", session.getSessionId()); + sendEventMap(this.getReactApplicationContext(), session.getSessionId() + ":" + sessionPreface + "onConnectionCreated", connectionInfo); printLogs("onConnectionCreated: Connection Created: "+connection.getConnectionId()); } @@ -543,14 +569,15 @@ public void onConnectionDestroyed(Session session, Connection connection) { ConcurrentHashMap mConnections = sharedState.getConnections(); mConnections.remove(connection.getConnectionId()); WritableMap connectionInfo = EventUtils.prepareJSConnectionMap(connection); - sendEventMap(this.getReactApplicationContext(), sessionPreface + "onConnectionDestroyed", connectionInfo); + connectionInfo.putString("sessionId", session.getSessionId()); + sendEventMap(this.getReactApplicationContext(), session.getSessionId() + ":" + sessionPreface + "onConnectionDestroyed", connectionInfo); printLogs("onConnectionDestroyed: Connection Destroyed: "+connection.getConnectionId()); } @Override public void onStreamDropped(Session session, Stream stream) { WritableMap streamInfo = EventUtils.prepareJSStreamMap(stream); - sendEventMap(this.getReactApplicationContext(), sessionPreface + "onStreamDropped", streamInfo); + sendEventMap(this.getReactApplicationContext(), session.getSessionId() + ":" + sessionPreface + "onStreamDropped", streamInfo); printLogs("onStreamDropped: Stream Dropped: "+stream.getStreamId() +" in session: "+session.getSessionId()); } @@ -688,7 +715,8 @@ public void onSignalReceived(Session session, String type, String data, Connecti if(connection != null) { signalInfo.putString("connectionId", connection.getConnectionId()); } - sendEventMap(this.getReactApplicationContext(), sessionPreface + "onSignalReceived", signalInfo); + signalInfo.putString("sessionId", session.getSessionId()); + sendEventMap(this.getReactApplicationContext(), session.getSessionId() + ":" + sessionPreface + "onSignalReceived", signalInfo); printLogs("onSignalReceived: Data: " + data + " Type: " + type); } @@ -825,14 +853,14 @@ public void onVideoDataReceived(SubscriberKit subscriber) { public void onStreamHasAudioChanged(Session session, Stream stream, boolean Audio) { WritableMap eventData = EventUtils.prepareStreamPropertyChangedEventData("hasAudio", !Audio, Audio, stream); - sendEventMap(this.getReactApplicationContext(), sessionPreface + "onStreamPropertyChanged", eventData); + sendEventMap(this.getReactApplicationContext(), session.getSessionId() + ":" + sessionPreface + "onStreamPropertyChanged", eventData); printLogs("onStreamHasAudioChanged"); } @Override public void onStreamHasVideoChanged(Session session, Stream stream, boolean Video) { WritableMap eventData = EventUtils.prepareStreamPropertyChangedEventData("hasVideo", !Video, Video, stream); - sendEventMap(this.getReactApplicationContext(), sessionPreface + "onStreamPropertyChanged", eventData); + sendEventMap(this.getReactApplicationContext(), session.getSessionId() + ":" + sessionPreface + "onStreamPropertyChanged", eventData); printLogs("onStreamHasVideoChanged"); } @@ -849,7 +877,7 @@ public void onStreamVideoDimensionsChanged(Session session, Stream stream, int w newVideoDimensions.putInt("height", height); newVideoDimensions.putInt("width", width); WritableMap eventData = EventUtils.prepareStreamPropertyChangedEventData("videoDimensions", oldVideoDimensions, newVideoDimensions, stream); - sendEventMap(this.getReactApplicationContext(), sessionPreface + "onStreamPropertyChanged", eventData); + sendEventMap(this.getReactApplicationContext(), session.getSessionId() + ":" + sessionPreface + "onStreamPropertyChanged", eventData); printLogs("onStreamVideoDimensionsChanged"); } @@ -858,10 +886,9 @@ public void onStreamVideoDimensionsChanged(Session session, Stream stream, int w public void onStreamVideoTypeChanged(Session session, Stream stream, Stream.StreamVideoType videoType) { ConcurrentHashMap mSubscriberStreams = sharedState.getSubscriberStreams(); - Stream mStream = mSubscriberStreams.get(stream.getStreamId()); String oldVideoType = stream.getStreamVideoType().toString(); WritableMap eventData = EventUtils.prepareStreamPropertyChangedEventData("videoType", oldVideoType, videoType.toString(), stream); - sendEventMap(this.getReactApplicationContext(), sessionPreface + "onStreamPropertyChanged", eventData); + sendEventMap(this.getReactApplicationContext(), session.getSessionId() + ":" + sessionPreface + "onStreamPropertyChanged", eventData); printLogs("onStreamVideoTypeChanged"); } diff --git a/android/src/main/java/com/opentokreactnative/OTSubscriberLayout.java b/android/src/main/java/com/opentokreactnative/OTSubscriberLayout.java index 910af085..79b72d09 100644 --- a/android/src/main/java/com/opentokreactnative/OTSubscriberLayout.java +++ b/android/src/main/java/com/opentokreactnative/OTSubscriberLayout.java @@ -26,11 +26,21 @@ public OTSubscriberLayout(ThemedReactContext reactContext) { public void createSubscriberView(String streamId) { ConcurrentHashMap mSubscribers = sharedState.getSubscribers(); - String pubOrSub = sharedState.getAndroidOnTop(); - String zOrder = sharedState.getAndroidZOrder(); + ConcurrentHashMap androidOnTopMap = sharedState.getAndroidOnTopMap(); + ConcurrentHashMap androidZOrderMap = sharedState.getAndroidZOrderMap(); Subscriber mSubscriber = mSubscribers.get(streamId); FrameLayout mSubscriberViewContainer = new FrameLayout(getContext()); + String pubOrSub = ""; + String zOrder = ""; if (mSubscriber != null) { + if (mSubscriber.getSession() != null) { + if (androidOnTopMap.get(mSubscriber.getSession().getSessionId()) != null) { + pubOrSub = androidOnTopMap.get(mSubscriber.getSession().getSessionId()); + } + if (androidZOrderMap.get(mSubscriber.getSession().getSessionId()) != null) { + zOrder = androidZOrderMap.get(mSubscriber.getSession().getSessionId()); + } + } mSubscriber.setStyle(BaseVideoRenderer.STYLE_VIDEO_SCALE, BaseVideoRenderer.STYLE_VIDEO_FILL); if (pubOrSub.equals("subscriber") && mSubscriber.getView() instanceof GLSurfaceView) { diff --git a/android/src/main/java/com/opentokreactnative/utils/EventUtils.java b/android/src/main/java/com/opentokreactnative/utils/EventUtils.java index d996ca8d..166f3c9b 100644 --- a/android/src/main/java/com/opentokreactnative/utils/EventUtils.java +++ b/android/src/main/java/com/opentokreactnative/utils/EventUtils.java @@ -30,6 +30,7 @@ public static WritableMap prepareJSStreamMap(Stream stream) { streamInfo.putInt("width", stream.getVideoWidth()); streamInfo.putString("creationTime", stream.getCreationTime().toString()); streamInfo.putString("connectionId", stream.getConnection().getConnectionId()); + streamInfo.putString("sessionId", stream.getSession().getSessionId()); streamInfo.putMap("connection", prepareJSConnectionMap(stream.getConnection())); streamInfo.putString("name", stream.getName()); streamInfo.putBoolean("hasAudio", stream.hasAudio()); diff --git a/docs/EventData.md b/docs/EventData.md index 6a0f06d8..65be8d3e 100644 --- a/docs/EventData.md +++ b/docs/EventData.md @@ -10,6 +10,7 @@ You can find the structure of the object below: archive = { archiveId: '', name: '', + sessionId: '', }; ``` @@ -63,6 +64,7 @@ You can find the structure of the object below: }, hasAudio: '', hasVideo: '', + sessionId: '', creationTime: '', height: '', width: '', @@ -85,6 +87,7 @@ You can find the structure of the object below: }, hasAudio: '', hasVideo: '', + sessionId: '', creationTime: '', height: '', width: '', diff --git a/ios/OpenTokReactNative.xcodeproj/project.pbxproj b/ios/OpenTokReactNative.xcodeproj/project.pbxproj index cedcec60..b798bc7c 100644 --- a/ios/OpenTokReactNative.xcodeproj/project.pbxproj +++ b/ios/OpenTokReactNative.xcodeproj/project.pbxproj @@ -16,11 +16,12 @@ 9C6F95D120ACE90E00FEAD68 /* OTPublisherManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C6F95C520ACE90E00FEAD68 /* OTPublisherManager.swift */; }; 9C6F95D220ACE90E00FEAD68 /* OTSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C6F95C620ACE90E00FEAD68 /* OTSessionManager.swift */; }; 9C6F95D320ACE90E00FEAD68 /* OTSubscriberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C6F95C720ACE90E00FEAD68 /* OTSubscriberView.swift */; }; - 9C6F95D420ACE90E00FEAD68 /* OTScreenCapturer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C6F95C920ACE90E00FEAD68 /* OTScreenCapturer.swift */; }; 9C6F95D520ACE90E00FEAD68 /* OTRN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C6F95CB20ACE90E00FEAD68 /* OTRN.swift */; }; 9C6F95D620ACE90E00FEAD68 /* OTPublisher.m in Sources */ = {isa = PBXBuildFile; fileRef = 9C6F95CC20ACE90E00FEAD68 /* OTPublisher.m */; }; 9C86849D218E2B5A004679BA /* EventUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C86849C218E2B5A004679BA /* EventUtils.swift */; }; 9C86850F218E3433004679BA /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C86850E218E3433004679BA /* Utils.swift */; }; + DED80B3522DFB01F0094CDC9 /* TBScreenCapture.m in Sources */ = {isa = PBXBuildFile; fileRef = DED80B3322DFB01F0094CDC9 /* TBScreenCapture.m */; }; + DED80B3822DFB02F0094CDC9 /* OTScreenCapture.m in Sources */ = {isa = PBXBuildFile; fileRef = DED80B3622DFB02E0094CDC9 /* OTScreenCapture.m */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -49,12 +50,13 @@ 9C6F95C620ACE90E00FEAD68 /* OTSessionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTSessionManager.swift; sourceTree = ""; }; 9C6F95C720ACE90E00FEAD68 /* OTSubscriberView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTSubscriberView.swift; sourceTree = ""; }; 9C6F95C820ACE90E00FEAD68 /* OTSubscriber.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OTSubscriber.h; sourceTree = ""; }; - 9C6F95C920ACE90E00FEAD68 /* OTScreenCapturer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTScreenCapturer.swift; sourceTree = ""; }; 9C6F95CA20ACE90E00FEAD68 /* OTPublisher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OTPublisher.h; sourceTree = ""; }; 9C6F95CB20ACE90E00FEAD68 /* OTRN.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTRN.swift; sourceTree = ""; }; 9C6F95CC20ACE90E00FEAD68 /* OTPublisher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OTPublisher.m; sourceTree = ""; }; 9C86849C218E2B5A004679BA /* EventUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventUtils.swift; sourceTree = ""; }; 9C86850E218E3433004679BA /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; + DED80B3622DFB02E0094CDC9 /* OTScreenCapture.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OTScreenCapture.m; sourceTree = ""; }; + DED80B3722DFB02F0094CDC9 /* OTScreenCapture.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OTScreenCapture.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -93,7 +95,6 @@ 9C6F95C520ACE90E00FEAD68 /* OTPublisherManager.swift */, 9C6F95C320ACE90E00FEAD68 /* OTPublisherView.swift */, 9C6F95CB20ACE90E00FEAD68 /* OTRN.swift */, - 9C6F95C920ACE90E00FEAD68 /* OTScreenCapturer.swift */, 9C6F95C120ACE90D00FEAD68 /* OTSessionManager.m */, 9C6F95C620ACE90E00FEAD68 /* OTSessionManager.swift */, 9C6F95C820ACE90E00FEAD68 /* OTSubscriber.h */, @@ -103,6 +104,8 @@ 9C6F95B720ACE8BE00FEAD68 /* OpenTokReactNative.h */, 9C6F95B820ACE8BE00FEAD68 /* OpenTokReactNative.m */, 9C6F95C020ACE90D00FEAD68 /* OpenTokReactNative-Bridging-Header.h */, + DED80B3722DFB02F0094CDC9 /* OTScreenCapture.h */, + DED80B3622DFB02E0094CDC9 /* OTScreenCapture.m */, ); path = OpenTokReactNative; sourceTree = ""; @@ -186,7 +189,8 @@ 9C6F95CD20ACE90E00FEAD68 /* OTSessionManager.m in Sources */, 9C6F95D620ACE90E00FEAD68 /* OTPublisher.m in Sources */, 9C6F95D520ACE90E00FEAD68 /* OTRN.swift in Sources */, - 9C6F95D420ACE90E00FEAD68 /* OTScreenCapturer.swift in Sources */, + 9C6F95CD20ACE90E00FEAD68 /* OTSessionManager.m in Sources */, + DED80B3822DFB02F0094CDC9 /* OTScreenCapture.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/OpenTokReactNative/OTRN.swift b/ios/OpenTokReactNative/OTRN.swift index 42299955..14d60b2f 100644 --- a/ios/OpenTokReactNative/OTRN.swift +++ b/ios/OpenTokReactNative/OTRN.swift @@ -10,12 +10,14 @@ import Foundation class OTRN : NSObject { static let sharedState = OTRN() - var session: OTSession? + var sessions = [String: OTSession]() var subscriberStreams = [String: OTStream]() var subscribers = [String: OTSubscriber]() var publishers = [String: OTPublisher]() var publisherStreams = [String: OTStream]() var publisherDestroyedCallbacks = [String: RCTResponseSenderBlock]() + var sessionConnectCallbacks = [String: RCTResponseSenderBlock]() + var sessionDisconnectCallbacks = [String: RCTResponseSenderBlock]() var isPublishing = [String: Bool]() var streamObservers = [String: [NSKeyValueObservation]]() var connections = [String: OTConnection]() diff --git a/ios/OpenTokReactNative/OTScreenCapture.h b/ios/OpenTokReactNative/OTScreenCapture.h new file mode 100644 index 00000000..0cc28476 --- /dev/null +++ b/ios/OpenTokReactNative/OTScreenCapture.h @@ -0,0 +1,38 @@ +// +// OTScreenCapture.h +// Screen-Sharing +// +// Copyright (c) 2014 TokBox Inc. All rights reserved. +// + +#import +#import + +@protocol OTVideoCapture; + +// defines for image scaling +// From https://bugs.chromium.org/p/webrtc/issues/detail?id=4643#c7 : +// Don't send any image larger than 1280px on either edge. Additionally, don't +// send any image with dimensions %16 != 0 +#define MAX_EDGE_SIZE_LIMIT 1280.0f +#define EDGE_DIMENSION_COMMON_FACTOR 16.0f + +/** + * Periodically sends video frames to an OpenTok Publisher by rendering the + * CALayer for a UIView. + */ +@interface OTScreenCapture : NSObject + +@property(readonly) UIView* view; + +/** + * Initializes a video capturer that will grab rendered stills of the view. + */ +- (instancetype)initWithView:(UIView*)view; + +// private: declared here for testing scaling & padding function ++ (void)dimensionsForInputSize:(CGSize)input + containerSize:(CGSize*)destContainerSize + drawRect:(CGRect*)destDrawRect; + +@end diff --git a/ios/OpenTokReactNative/OTScreenCapture.m b/ios/OpenTokReactNative/OTScreenCapture.m new file mode 100644 index 00000000..a8e804e0 --- /dev/null +++ b/ios/OpenTokReactNative/OTScreenCapture.m @@ -0,0 +1,353 @@ +// +// OTScreenCapture.m +// Screen-Sharing +// +// Copyright (c) 2014 TokBox Inc. All rights reserved. +// + +#include +#include +#import "OTScreenCapture.h" + +@implementation OTScreenCapture { + CMTime _minFrameDuration; + dispatch_queue_t _queue; + dispatch_source_t _timer; + + CVPixelBufferRef _pixelBuffer; + BOOL _capturing; + OTVideoFrame* _videoFrame; + UIView* _view; + +} + +@synthesize videoCaptureConsumer; + +#pragma mark - Class Lifecycle. + +- (instancetype)initWithView:(UIView *)view +{ + self = [super init]; + if (self) { + _view = view; + // Recommend sending 5 frames per second: Allows for higher image + // quality per frame + _minFrameDuration = CMTimeMake(1, 5); + _queue = dispatch_queue_create("SCREEN_CAPTURE", NULL); + + OTVideoFormat *format = [[OTVideoFormat alloc] init]; + [format setPixelFormat:OTPixelFormatARGB]; + + _videoFrame = [[OTVideoFrame alloc] initWithFormat:format]; + + } + return self; +} + +- (void)dealloc +{ + [self stopCapture]; + CVPixelBufferRelease(_pixelBuffer); +} + +#pragma mark - Private Methods + +/** + * Make sure receiving video frame container is setup for this image. + */ +- (void)checkImageSize:(CGImageRef)image { + CGFloat width = CGImageGetWidth(image); + CGFloat height = CGImageGetHeight(image); + + if (_videoFrame.format.imageHeight == height && + _videoFrame.format.imageWidth == width) + { + // don't rock the boat. if nothing has changed, don't update anything. + return; + } + + [_videoFrame.format.bytesPerRow removeAllObjects]; + [_videoFrame.format.bytesPerRow addObject:@(width * 4)]; + [_videoFrame.format setImageHeight:height]; + [_videoFrame.format setImageWidth:width]; + + CGSize frameSize = CGSizeMake(width, height); + NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys: + @NO, + kCVPixelBufferCGImageCompatibilityKey, + @NO, + kCVPixelBufferCGBitmapContextCompatibilityKey, + nil]; + + if (NULL != _pixelBuffer) { + CVPixelBufferRelease(_pixelBuffer); + } + + CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, + frameSize.width, + frameSize.height, + kCVPixelFormatType_32ARGB, + (__bridge CFDictionaryRef)(options), + &_pixelBuffer); + + NSParameterAssert(status == kCVReturnSuccess && _pixelBuffer != NULL); + +} + +#pragma mark - Capture lifecycle + +/** + * Allocate capture resources; in this case we're just setting up a timer and + * block to execute periodically to send video frames. + */ +- (void)initCapture { + __unsafe_unretained OTScreenCapture* _self = self; + _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _queue); + + dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), + 100ull * NSEC_PER_MSEC, 100ull * NSEC_PER_MSEC); + + dispatch_source_set_event_handler(_timer, ^{ + @autoreleasepool { + __block UIImage* screen = [_self screenshot]; + CGImageRef paddedScreen = [self resizeAndPadImage:screen]; + [_self consumeFrame:paddedScreen]; + } + }); +} + +- (void)releaseCapture { + _timer = nil; +} + +- (int32_t)startCapture +{ + _capturing = YES; + + if (_timer) { + dispatch_resume(_timer); + } + + return 0; +} + +- (int32_t)stopCapture +{ + _capturing = NO; + + dispatch_sync(_queue, ^{ + if (self->_timer) { + dispatch_source_cancel(self->_timer); + } + }); + + return 0; +} + +- (BOOL)isCaptureStarted +{ + return _capturing; +} + +#pragma mark - Screen capture implementation + +- (CVPixelBufferRef)pixelBufferFromCGImage:(CGImageRef)image +{ + CGFloat width = CGImageGetWidth(image); + CGFloat height = CGImageGetHeight(image); + CGSize frameSize = CGSizeMake(width, height); + CVPixelBufferLockBaseAddress(_pixelBuffer, 0); + void *pxdata = CVPixelBufferGetBaseAddress(_pixelBuffer); + + CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB(); + CGContextRef context = + CGBitmapContextCreate(pxdata, + frameSize.width, + frameSize.height, + 8, + CVPixelBufferGetBytesPerRow(_pixelBuffer), + rgbColorSpace, + kCGImageAlphaPremultipliedFirst | + kCGBitmapByteOrder32Little); + + + CGContextDrawImage(context, CGRectMake(0, 0, width, height), image); + CGColorSpaceRelease(rgbColorSpace); + CGContextRelease(context); + + CVPixelBufferUnlockBaseAddress(_pixelBuffer, 0); + + return _pixelBuffer; +} + +- (int32_t)captureSettings:(OTVideoFormat*)videoFormat +{ + videoFormat.pixelFormat = OTPixelFormatARGB; + return 0; +} + ++ (void)dimensionsForInputSize:(CGSize)input + containerSize:(CGSize*)destContainerSize + drawRect:(CGRect*)destDrawRect +{ + CGFloat sourceWidth = input.width; + CGFloat sourceHeight = input.height; + double sourceAspectRatio = sourceWidth / sourceHeight; + + CGFloat destContainerWidth = sourceWidth; + CGFloat destContainerHeight = sourceHeight; + CGFloat destImageWidth = sourceWidth; + CGFloat destImageHeight = sourceHeight; + + // if image is wider than tall and width breaks edge size limit + if (MAX_EDGE_SIZE_LIMIT < sourceWidth && sourceAspectRatio >= 1.0) { + destContainerWidth = MAX_EDGE_SIZE_LIMIT; + destContainerHeight = destContainerWidth / sourceAspectRatio; + if (0 != fmod(destContainerHeight, EDGE_DIMENSION_COMMON_FACTOR)) { + // add padding to make height % 16 == 0 + destContainerHeight += + (EDGE_DIMENSION_COMMON_FACTOR - fmod(destContainerHeight, + EDGE_DIMENSION_COMMON_FACTOR)); + } + destImageWidth = destContainerWidth; + destImageHeight = destContainerWidth / sourceAspectRatio; + } + + // if image is taller than wide and height breaks edge size limit + if (MAX_EDGE_SIZE_LIMIT < destContainerHeight && sourceAspectRatio <= 1.0) { + destContainerHeight = MAX_EDGE_SIZE_LIMIT; + destContainerWidth = destContainerHeight * sourceAspectRatio; + if (0 != fmod(destContainerWidth, EDGE_DIMENSION_COMMON_FACTOR)) { + // add padding to make width % 16 == 0 + destContainerWidth += + (EDGE_DIMENSION_COMMON_FACTOR - fmod(destContainerWidth, + EDGE_DIMENSION_COMMON_FACTOR)); + } + destImageHeight = destContainerHeight; + destImageWidth = destContainerHeight * sourceAspectRatio; + } + + // ensure the dimensions of the resulting container are safe + if (fmod(destContainerWidth, EDGE_DIMENSION_COMMON_FACTOR) != 0) { + double remainder = fmod(destContainerWidth, + EDGE_DIMENSION_COMMON_FACTOR); + // increase the edge size only if doing so does not break the edge limit + if (destContainerWidth + (EDGE_DIMENSION_COMMON_FACTOR - remainder) > + MAX_EDGE_SIZE_LIMIT) + { + destContainerWidth -= remainder; + } else { + destContainerWidth += EDGE_DIMENSION_COMMON_FACTOR - remainder; + } + } + // ensure the dimensions of the resulting container are safe + if (fmod(destContainerHeight, EDGE_DIMENSION_COMMON_FACTOR) != 0) { + double remainder = fmod(destContainerHeight, + EDGE_DIMENSION_COMMON_FACTOR); + // increase the edge size only if doing so does not break the edge limit + if (destContainerHeight + (EDGE_DIMENSION_COMMON_FACTOR - remainder) > + MAX_EDGE_SIZE_LIMIT) + { + destContainerHeight -= remainder; + } else { + destContainerHeight += EDGE_DIMENSION_COMMON_FACTOR - remainder; + } + } + + destContainerSize->width = destContainerWidth; + destContainerSize->height = destContainerHeight; + + // scale and recenter source image to fit in destination container + if (sourceAspectRatio > 1.0) { + destDrawRect->origin.x = 0; + destDrawRect->origin.y = + (destContainerHeight - destImageHeight) / 2; + destDrawRect->size.width = destContainerWidth; + destDrawRect->size.height = + destContainerWidth / sourceAspectRatio; + } else { + destDrawRect->origin.x = + (destContainerWidth - destImageWidth) / 2; + destDrawRect->origin.y = 0; + destDrawRect->size.height = destContainerHeight; + destDrawRect->size.width = + destContainerHeight * sourceAspectRatio; + } + +} + +- (CGImageRef)resizeAndPadImage:(UIImage*)sourceUIImage { + CGImageRef sourceCGImage = [sourceUIImage CGImage]; + CGFloat sourceWidth = CGImageGetWidth(sourceCGImage); + CGFloat sourceHeight = CGImageGetHeight(sourceCGImage); + CGSize sourceSize = CGSizeMake(sourceWidth, sourceHeight); + CGSize destContainerSize = CGSizeZero; + CGRect destRectForSourceImage = CGRectZero; + + [OTScreenCapture dimensionsForInputSize:sourceSize + containerSize:&destContainerSize + drawRect:&destRectForSourceImage]; + + UIGraphicsBeginImageContextWithOptions(destContainerSize, NO, 1.0); + CGContextRef context = UIGraphicsGetCurrentContext(); + + // flip source image to match destination coordinate system + CGContextScaleCTM(context, 1.0, -1.0); + CGContextTranslateCTM(context, 0, -destRectForSourceImage.size.height); + CGContextDrawImage(context, destRectForSourceImage, sourceCGImage); + + // Clean up and get the new image. + UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return [newImage CGImage]; +} + +- (UIImage *)screenshot +{ + UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, 0.0); + [self.view drawViewHierarchyInRect:self.view.bounds afterScreenUpdates:NO]; + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return image; +} + +- (void) consumeFrame:(CGImageRef)frame { + + [self checkImageSize:frame]; + + static mach_timebase_info_data_t time_info; + uint64_t time_stamp = 0; + + if (!(_capturing && self.videoCaptureConsumer)) { + return; + } + + if (time_info.denom == 0) { + (void) mach_timebase_info(&time_info); + } + + time_stamp = mach_absolute_time(); + time_stamp *= time_info.numer; + time_stamp /= time_info.denom; + + CMTime time = CMTimeMake(time_stamp, 1000); + CVImageBufferRef ref = [self pixelBufferFromCGImage:frame]; + + CVPixelBufferLockBaseAddress(ref, 0); + + _videoFrame.timestamp = time; + _videoFrame.format.estimatedFramesPerSecond = + _minFrameDuration.timescale / _minFrameDuration.value; + _videoFrame.format.estimatedCaptureDelay = 100; + _videoFrame.orientation = OTVideoOrientationUp; + + [_videoFrame clearPlanes]; + [_videoFrame.planes addPointer:CVPixelBufferGetBaseAddress(ref)]; + [self.videoCaptureConsumer consumeFrame:_videoFrame]; + + CVPixelBufferUnlockBaseAddress(ref, 0); +} + + +@end diff --git a/ios/OpenTokReactNative/OTScreenCapturer.swift b/ios/OpenTokReactNative/OTScreenCapturer.swift deleted file mode 100644 index 65ddf5c7..00000000 --- a/ios/OpenTokReactNative/OTScreenCapturer.swift +++ /dev/null @@ -1,244 +0,0 @@ -// -// OTScreenCapturer.swift -// -// Created by Manik Sachdeva on 04/30/18. -// Copyright © 2018 Facebook. All rights reserved. -// - -import Foundation - -class OTScreenCapturer: NSObject, OTVideoCapture { - var videoCaptureConsumer: OTVideoCaptureConsumer? - - let MAX_EDGE_SIZE_LIMIT: CGFloat = 1280.0 - let EDGE_DIMENSION_COMMON_FACTOR: CGFloat = 16.0 - - fileprivate let captureView: UIView - fileprivate let captureQueue = DispatchQueue(label: "ot-screen-capture") - fileprivate var timer: DispatchSourceTimer - fileprivate var capturing: Bool = false - fileprivate var videoFrame = OTVideoFrame(format: OTVideoFormat(argbWithWidth: 0, height: 0)) - fileprivate var pixelBuffer: CVPixelBuffer? - - init(withView: UIView) { - captureView = withView - timer = DispatchSource.makeTimerSource(flags: .strict, queue: captureQueue) - } - fileprivate func screenShot(completion: @escaping (_ image: UIImage) -> ()) { - DispatchQueue.main.async { - UIGraphicsBeginImageContextWithOptions(self.captureView.bounds.size, false, 0.0) - self.captureView.drawHierarchy(in: self.captureView.bounds, afterScreenUpdates: false) - let image = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext(); - completion(image!) - } - } - - - fileprivate func resizeAndPad(image img: UIImage) -> CGImage { - let source = img.cgImage! - let size = CGSize(width: source.width, height: source.height) - let destSizes = dimensions(forInputSize: size) - - UIGraphicsBeginImageContextWithOptions(destSizes.container, false, 1.0) - let ctx = UIGraphicsGetCurrentContext() - - ctx?.scaleBy(x: 1, y: -1) - ctx?.translateBy(x: 0, y: -destSizes.rect.size.height) - ctx?.draw(source, in: destSizes.rect) - - let newImage = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - - return (newImage?.cgImage)! - } - - fileprivate func consume(frame: CGImage) { - checkSize(forImage: frame) - - if !capturing { - return - } - - let timeStamp = mach_absolute_time() - let time = CMTime(seconds: Double(timeStamp), preferredTimescale: 1000) - let ref = pixelBuffer(fromCGImage: frame) - - CVPixelBufferLockBaseAddress(ref, CVPixelBufferLockFlags(rawValue: 0)) - - videoFrame.timestamp = time - videoFrame.format?.estimatedCaptureDelay = 100 - videoFrame.orientation = .up - - videoFrame.clearPlanes() - videoFrame.planes?.addPointer(CVPixelBufferGetBaseAddress(ref)) - videoCaptureConsumer?.consumeFrame(videoFrame) - - CVPixelBufferUnlockBaseAddress(ref, CVPixelBufferLockFlags(rawValue: CVOptionFlags(0))) - } - - // MARK: - OTVideoCapture protocol - func consumeImage(image: UIImage) { - let padded = self.resizeAndPad(image: image) - self.consume(frame: padded) - } - - func initCapture() { - timer.setEventHandler { - self.screenShot(completion: self.consumeImage) - } - timer.scheduleRepeating(deadline: DispatchTime.now(), interval: DispatchTimeInterval.milliseconds(100)) - } - - func start() -> Int32 { - capturing = true - captureQueue.sync { - timer.resume() - } - return 0 - } - - func stop() -> Int32 { - capturing = false - captureQueue.sync { - timer.cancel() - } - return 0 - } - - func releaseCapture() { - timer.cancel() - } - - func isCaptureStarted() -> Bool { - return capturing - } - - func captureSettings(_ videoFormat: OTVideoFormat) -> Int32 { - videoFormat.pixelFormat = .ARGB - return 0 - } -} - -// MARK: - Image Utils -extension OTScreenCapturer { - fileprivate func pixelBuffer(fromCGImage img: CGImage) -> CVPixelBuffer { - let frameSize = CGSize(width: img.width, height: img.height) - CVPixelBufferLockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: CVOptionFlags(0))) - let pxdata = CVPixelBufferGetBaseAddress(pixelBuffer!) - - let rgbColorSpace = CGColorSpaceCreateDeviceRGB() - let context = - CGContext(data: pxdata, - width: Int(frameSize.width), - height: Int(frameSize.height), - bitsPerComponent: 8, - bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer!), - space: rgbColorSpace, - bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue) - - - context?.draw(img, in: CGRect(x: 0, y: 0, width: img.width, height: img.height)) - - CVPixelBufferUnlockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: CVOptionFlags(0))) - - return pixelBuffer!; - } - - fileprivate func dimensions(forInputSize size: CGSize) -> (container: CGSize, rect: CGRect) { - let aspect = size.width / size.height - - var destContainer = CGSize(width: size.width, height: size.height) - var destFrame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)) - - // if image is wider than tall and width breaks edge size limit - if MAX_EDGE_SIZE_LIMIT < size.width && aspect >= 1.0 { - destContainer.width = MAX_EDGE_SIZE_LIMIT - destContainer.height = destContainer.width / aspect - if 0 != fmod(destContainer.height, EDGE_DIMENSION_COMMON_FACTOR) { - destContainer.height += - (EDGE_DIMENSION_COMMON_FACTOR - fmod(destContainer.height, EDGE_DIMENSION_COMMON_FACTOR)) - } - destFrame.size.width = destContainer.width - destFrame.size.height = destContainer.width / aspect - } - - // ensure the dimensions of the resulting container are safe - if (fmod(destContainer.width, EDGE_DIMENSION_COMMON_FACTOR) != 0) { - let remainder = fmod(destContainer.width, - EDGE_DIMENSION_COMMON_FACTOR); - // increase the edge size only if doing so does not break the edge limit - if (destContainer.width + (EDGE_DIMENSION_COMMON_FACTOR - remainder) > - MAX_EDGE_SIZE_LIMIT) - { - destContainer.width -= remainder; - } else { - destContainer.width += EDGE_DIMENSION_COMMON_FACTOR - remainder; - } - } - // ensure the dimensions of the resulting container are safe - if (fmod(destContainer.height, EDGE_DIMENSION_COMMON_FACTOR) != 0) { - let remainder = fmod(destContainer.height, - EDGE_DIMENSION_COMMON_FACTOR); - // increase the edge size only if doing so does not break the edge limit - if (destContainer.height + (EDGE_DIMENSION_COMMON_FACTOR - remainder) > - MAX_EDGE_SIZE_LIMIT) - { - destContainer.height -= remainder; - } else { - destContainer.height += EDGE_DIMENSION_COMMON_FACTOR - remainder; - } - } - - destFrame.size.width = destContainer.width; - destFrame.size.height = destContainer.height; - - // scale and recenter source image to fit in destination container - if (aspect > 1.0) { - destFrame.origin.x = 0; - destFrame.origin.y = - (destContainer.height - destContainer.width) / 2; - destFrame.size.width = destContainer.width; - destFrame.size.height = - destContainer.width / aspect; - } else { - destFrame.origin.x = - (destContainer.width - destContainer.width) / 2; - destFrame.origin.y = 0; - destFrame.size.height = destContainer.height; - destFrame.size.width = - destContainer.height * aspect; - } - - return (destContainer, destFrame) - } - - fileprivate func checkSize(forImage img: CGImage) { - guard let frameFormat = videoFrame.format, frameFormat.imageHeight != UInt32(img.height), - frameFormat.imageWidth != UInt32(img.width) - else { - return - } - - frameFormat.bytesPerRow.removeAllObjects() - frameFormat.bytesPerRow.addObjects(from: [img.width * 4]) - frameFormat.imageWidth = UInt32(img.width) - frameFormat.imageHeight = UInt32(img.height) - - let frameSize = CGSize(width: img.width, height: img.height) - let options: Dictionary = [ - kCVPixelBufferCGImageCompatibilityKey as String: false, - kCVPixelBufferCGBitmapContextCompatibilityKey as String: false - ] - - let status = CVPixelBufferCreate(kCFAllocatorDefault, - Int(frameSize.width), - Int(frameSize.height), - kCVPixelFormatType_32ARGB, - options as CFDictionary, - &pixelBuffer) - - assert(status == kCVReturnSuccess && pixelBuffer != nil) - } -} - diff --git a/ios/OpenTokReactNative/OTSessionManager.m b/ios/OpenTokReactNative/OTSessionManager.m index 87ab227b..ac3100ae 100644 --- a/ios/OpenTokReactNative/OTSessionManager.m +++ b/ios/OpenTokReactNative/OTSessionManager.m @@ -18,14 +18,16 @@ @interface RCT_EXTERN_MODULE(OTSessionManager, RCTEventEmitter) sessionId:(NSString*)sessionId sessionOptions:(NSDictionary*)sessionOptions) RCT_EXTERN_METHOD(connect: - (NSString*)token + (NSString*)sessionId + token:(NSString*)token callback:(RCTResponseSenderBlock*)callback) RCT_EXTERN_METHOD(initPublisher: (NSString*)publisherId properties:(NSDictionary*)properties callback:(RCTResponseSenderBlock*)callback) RCT_EXTERN_METHOD(publish: - (NSString*)publisherId + (NSString*)sessionId + publisherId:(NSString*)publisherId callback:(RCTResponseSenderBlock*)callback) RCT_EXTERN_METHOD(subscribeToStream: (NSString*)streamId @@ -35,7 +37,8 @@ @interface RCT_EXTERN_MODULE(OTSessionManager, RCTEventEmitter) (NSString*)streamId callback:(RCTResponseSenderBlock*)callback) RCT_EXTERN_METHOD(disconnectSession: - (RCTResponseSenderBlock*)callback) + (NSString*)sessionId + callback:(RCTResponseSenderBlock*)callback) RCT_EXTERN_METHOD(publishAudio: (NSString*)publisherId pubAudio:(BOOL)pubAudio) @@ -56,7 +59,8 @@ @interface RCT_EXTERN_MODULE(OTSessionManager, RCTEventEmitter) RCT_EXTERN_METHOD(removeNativeEvents: (NSArray*)events) RCT_EXTERN_METHOD(sendSignal: - (NSDictionary*)properties + (NSString*)sessionId + signal:(NSDictionary*)signal callback:(RCTResponseSenderBlock*)callback) RCT_EXTERN_METHOD(destroyPublisher: (NSString*)publisherId @@ -66,7 +70,8 @@ @interface RCT_EXTERN_MODULE(OTSessionManager, RCTEventEmitter) RCT_EXTERN_METHOD(removeJSComponentEvents: (NSArray*)events) RCT_EXTERN_METHOD(getSessionInfo: - (RCTResponseSenderBlock*)callback) + (NSString*)sessionId + callback:(RCTResponseSenderBlock*)callback) RCT_EXTERN_METHOD(enableLogs: (BOOL)logLevel) @end diff --git a/ios/OpenTokReactNative/OTSessionManager.swift b/ios/OpenTokReactNative/OTSessionManager.swift index 91e3ea8d..a1c1d398 100644 --- a/ios/OpenTokReactNative/OTSessionManager.swift +++ b/ios/OpenTokReactNative/OTSessionManager.swift @@ -11,15 +11,15 @@ import Foundation @objc(OTSessionManager) class OTSessionManager: RCTEventEmitter { - var connectCallback: RCTResponseSenderBlock? - var disconnectCallback: RCTResponseSenderBlock? var jsEvents: [String] = []; var componentEvents: [String] = []; var logLevel: Bool = false; deinit { OTRN.sharedState.subscriberStreams.removeAll(); - OTRN.sharedState.session = nil; + OTRN.sharedState.sessions.removeAll(); + OTRN.sharedState.sessionConnectCallbacks.removeAll(); + OTRN.sharedState.sessionDisconnectCallbacks.removeAll(); OTRN.sharedState.isPublishing.removeAll(); OTRN.sharedState.publishers.removeAll(); OTRN.sharedState.subscribers.removeAll(); @@ -41,16 +41,21 @@ class OTSessionManager: RCTEventEmitter { @objc func initSession(_ apiKey: String, sessionId: String, sessionOptions: Dictionary) -> Void { let settings = OTSessionSettings() settings.connectionEventsSuppressed = Utils.sanitizeBooleanProperty(sessionOptions["connectionEventsSuppressed"] as Any); - OTRN.sharedState.session = OTSession(apiKey: apiKey, sessionId: sessionId, delegate: self, settings: settings) + OTRN.sharedState.sessions.updateValue(OTSession(apiKey: apiKey, sessionId: sessionId, delegate: self, settings: settings)!, forKey: sessionId); } - @objc func connect(_ token: String, callback: @escaping RCTResponseSenderBlock) -> Void { + @objc func connect(_ sessionId: String, token: String, callback: @escaping RCTResponseSenderBlock) -> Void { var error: OTError? - OTRN.sharedState.session?.connect(withToken: token, error: &error) + guard let session = OTRN.sharedState.sessions[sessionId] else { + let errorInfo = EventUtils.createErrorMessage("Error connecting to session. Could not find native session instance") + callback([errorInfo]); + return + } + session.connect(withToken: token, error: &error) if let err = error { self.dispatchErrorViaCallback(callback, error: err) } else { - connectCallback = callback + OTRN.sharedState.sessionConnectCallbacks[sessionId] = callback; } } @@ -78,7 +83,7 @@ class OTSessionManager: RCTEventEmitter { return } publisher.videoType = .screen; - publisher.videoCapture = OTScreenCapturer(withView: (screenView)) + publisher.videoCapture = OTScreenCapture(view: (screenView)) } else if let cameraPosition = properties["cameraPosition"] as? String { publisher.cameraPosition = cameraPosition == "front" ? .front : .back; } @@ -90,14 +95,19 @@ class OTSessionManager: RCTEventEmitter { } } - @objc func publish(_ publisherId: String, callback: RCTResponseSenderBlock) -> Void { + @objc func publish(_ sessionId: String, publisherId: String, callback: RCTResponseSenderBlock) -> Void { var error: OTError? guard let publisher = OTRN.sharedState.publishers[publisherId] else { let errorInfo = EventUtils.createErrorMessage("Error publishing. Could not find native publisher instance") callback([errorInfo]); return } - OTRN.sharedState.session?.publish(publisher, error: &error) + guard let session = OTRN.sharedState.sessions[sessionId] else { + let errorInfo = EventUtils.createErrorMessage("Error connecting to session. Could not find native session instance") + callback([errorInfo]); + return + } + session.publish(publisher, error: &error) if let err = error { dispatchErrorViaCallback(callback, error: err) } else { @@ -118,10 +128,15 @@ class OTSessionManager: RCTEventEmitter { callback([errorInfo]); return } + guard let session = OTRN.sharedState.sessions[stream.session.sessionId] else { + let errorInfo = EventUtils.createErrorMessage("Error subscribing to stream. Could not find native session instance") + callback([errorInfo]); + return + } OTRN.sharedState.subscribers.updateValue(subscriber, forKey: streamId) subscriber.networkStatsDelegate = self; subscriber.audioLevelDelegate = self; - OTRN.sharedState.session?.subscribe(subscriber, error: &error) + session.subscribe(subscriber, error: &error) subscriber.subscribeToAudio = Utils.sanitizeBooleanProperty(properties["subscribeToAudio"] as Any); subscriber.subscribeToVideo = Utils.sanitizeBooleanProperty(properties["subscribeToVideo"] as Any); if let err = error { @@ -148,13 +163,18 @@ class OTSessionManager: RCTEventEmitter { } - @objc func disconnectSession(_ callback: @escaping RCTResponseSenderBlock) -> Void { + @objc func disconnectSession(_ sessionId: String, callback: @escaping RCTResponseSenderBlock) -> Void { var error: OTError? - OTRN.sharedState.session?.disconnect(&error) + guard let session = OTRN.sharedState.sessions[sessionId] else { + let errorInfo = EventUtils.createErrorMessage("Error disconnecting from session. Could not find native session instance") + callback([errorInfo]); + return + } + session.disconnect(&error) if let err = error { dispatchErrorViaCallback(callback, error: err) } else { - disconnectCallback = callback; + OTRN.sharedState.sessionDisconnectCallbacks[sessionId] = callback; } } @@ -205,14 +225,19 @@ class OTSessionManager: RCTEventEmitter { } } - @objc func sendSignal(_ signal: Dictionary, callback: RCTResponseSenderBlock ) -> Void { + @objc func sendSignal(_ sessionId: String, signal: Dictionary, callback: RCTResponseSenderBlock ) -> Void { var error: OTError? + guard let session = OTRN.sharedState.sessions[sessionId] else { + let errorInfo = EventUtils.createErrorMessage("Error sending signal. Could not find native session instance") + callback([errorInfo]); + return + } if let connectionId = signal["to"] { let connection = OTRN.sharedState.connections[connectionId] - OTRN.sharedState.session?.signal(withType: signal["type"], string: signal["data"], connection: connection, error: &error) + session.signal(withType: signal["type"], string: signal["data"], connection: connection, error: &error) } else { let connection: OTConnection? = nil - OTRN.sharedState.session?.signal(withType: signal["type"], string: signal["data"], connection: connection, error: &error) + session.signal(withType: signal["type"], string: signal["data"], connection: connection, error: &error) } if let err = error { dispatchErrorViaCallback(callback, error: err) @@ -224,14 +249,22 @@ class OTSessionManager: RCTEventEmitter { @objc func destroyPublisher(_ publisherId: String, callback: @escaping RCTResponseSenderBlock) -> Void { DispatchQueue.main.async { guard let publisher = OTRN.sharedState.publishers[publisherId] else { callback([NSNull()]); return } - guard let session = OTRN.sharedState.session else { - callback([NSNull()]); - return - } var error: OTError? if let isPublishing = OTRN.sharedState.isPublishing[publisherId] { - if (isPublishing && session.sessionConnectionStatus.rawValue == 1) { - session.unpublish(publisher, error: &error) + if (isPublishing) { + guard let sessionId = publisher.session?.sessionId else { + let errorInfo = EventUtils.createErrorMessage("Error destroying publisher. Could not find sessionId") + callback([errorInfo]); + return + } + guard let session = OTRN.sharedState.sessions[sessionId] else { + let errorInfo = EventUtils.createErrorMessage("Error destroying publisher. Could not find native session instance") + callback([errorInfo]); + return + } + if (session.sessionConnectionStatus.rawValue == 1) { + session.unpublish(publisher, error: &error) + } } } guard let err = error else { @@ -250,8 +283,8 @@ class OTSessionManager: RCTEventEmitter { } } - @objc func getSessionInfo(_ callback: RCTResponseSenderBlock) -> Void { - guard let session = OTRN.sharedState.session else { callback([NSNull()]); return } + @objc func getSessionInfo(_ sessionId: String, callback: RCTResponseSenderBlock) -> Void { + guard let session = OTRN.sharedState.sessions[sessionId] else { callback([NSNull()]); return } var sessionInfo: Dictionary = EventUtils.prepareJSSessionEventData(session); sessionInfo["connectionStatus"] = session.sessionConnectionStatus.rawValue; callback([sessionInfo]); @@ -281,7 +314,7 @@ class OTSessionManager: RCTEventEmitter { guard let stream = isPublisherStream ? OTRN.sharedState.publisherStreams[streamId] : OTRN.sharedState.subscriberStreams[streamId] else { return } let streamInfo: Dictionary = EventUtils.prepareJSStreamEventData(stream); let eventData: Dictionary = EventUtils.prepareStreamPropertyChangedEventData(changedProperty, oldValue: oldValue, newValue: newValue, stream: streamInfo); - self.emitEvent("\(EventUtils.sessionPreface)streamPropertyChanged", data: eventData) + self.emitEvent("\(stream.session.sessionId):\(EventUtils.sessionPreface)streamPropertyChanged", data: eventData) } func dispatchErrorViaCallback(_ callback: RCTResponseSenderBlock, error: OTError) { @@ -322,33 +355,37 @@ class OTSessionManager: RCTEventEmitter { extension OTSessionManager: OTSessionDelegate { func sessionDidConnect(_ session: OTSession) { - guard let callback = connectCallback else { return } + guard let callback = OTRN.sharedState.sessionConnectCallbacks[session.sessionId] else { return } callback([NSNull()]) let sessionInfo = EventUtils.prepareJSSessionEventData(session); - self.emitEvent("\(EventUtils.sessionPreface)sessionDidConnect", data: sessionInfo); + self.emitEvent("\(session.sessionId):\(EventUtils.sessionPreface)sessionDidConnect", data: sessionInfo); printLogs("OTRN: Session connected") } func sessionDidDisconnect(_ session: OTSession) { let sessionInfo = EventUtils.prepareJSSessionEventData(session); - self.emitEvent("\(EventUtils.sessionPreface)sessionDidDisconnect", data: sessionInfo); - guard let callback = disconnectCallback else { return } + self.emitEvent("\(session.sessionId):\(EventUtils.sessionPreface)sessionDidDisconnect", data: sessionInfo); + guard let callback = OTRN.sharedState.sessionDisconnectCallbacks[session.sessionId] else { return } callback([NSNull()]); - OTRN.sharedState.session?.delegate = nil; - OTRN.sharedState.session = nil; + session.delegate = nil; + OTRN.sharedState.sessions.removeValue(forKey: session.sessionId); + OTRN.sharedState.sessionDisconnectCallbacks.removeValue(forKey: session.sessionId); + OTRN.sharedState.sessionConnectCallbacks.removeValue(forKey: session.sessionId); printLogs("OTRN: Session disconnected") } func session(_ session: OTSession, connectionCreated connection: OTConnection) { OTRN.sharedState.connections.updateValue(connection, forKey: connection.connectionId) - let connectionInfo = EventUtils.prepareJSConnectionEventData(connection); - self.emitEvent("\(EventUtils.sessionPreface)connectionCreated", data: connectionInfo) + var connectionInfo = EventUtils.prepareJSConnectionEventData(connection); + connectionInfo["sessionId"] = session.sessionId; + self.emitEvent("\(session.sessionId):\(EventUtils.sessionPreface)connectionCreated", data: connectionInfo) printLogs("OTRN Session: A connection was created \(connection.connectionId)") } func session(_ session: OTSession, connectionDestroyed connection: OTConnection) { OTRN.sharedState.connections.removeValue(forKey: connection.connectionId) - let connectionInfo = EventUtils.prepareJSConnectionEventData(connection); - self.emitEvent("\(EventUtils.sessionPreface)connectionDestroyed", data: connectionInfo) + var connectionInfo = EventUtils.prepareJSConnectionEventData(connection); + connectionInfo["sessionId"] = session.sessionId; + self.emitEvent("\(session.sessionId):\(EventUtils.sessionPreface)connectionDestroyed", data: connectionInfo) printLogs("OTRN Session: A connection was destroyed") } @@ -356,7 +393,8 @@ extension OTSessionManager: OTSessionDelegate { var archiveInfo: Dictionary = [:]; archiveInfo["archiveId"] = archiveId; archiveInfo["name"] = name; - self.emitEvent("\(EventUtils.sessionPreface)archiveStartedWithId", data: archiveInfo) + archiveInfo["sessionId"] = session.sessionId; + self.emitEvent("\(session.sessionId):\(EventUtils.sessionPreface)archiveStartedWithId", data: archiveInfo) printLogs("OTRN Session: Archive started with \(archiveId)") } @@ -364,37 +402,40 @@ extension OTSessionManager: OTSessionDelegate { var archiveInfo: Dictionary = [:]; archiveInfo["archiveId"] = archiveId; archiveInfo["name"] = ""; - self.emitEvent("\(EventUtils.sessionPreface)archiveStoppedWithId", data: archiveInfo); + archiveInfo["sessionId"] = session.sessionId; + self.emitEvent("\(session.sessionId):\(EventUtils.sessionPreface)archiveStoppedWithId", data: archiveInfo); printLogs("OTRN Session: Archive stopped with \(archiveId)") } func sessionDidBeginReconnecting(_ session: OTSession) { - self.emitEvent("\(EventUtils.sessionPreface)sessionDidBeginReconnecting", data: [NSNull()]) + let sessionInfo = EventUtils.prepareJSSessionEventData(session); + self.emitEvent("\(session.sessionId):\(EventUtils.sessionPreface)sessionDidBeginReconnecting", data: sessionInfo) printLogs("OTRN Session: Session did begin reconnecting") } func sessionDidReconnect(_ session: OTSession) { - self.emitEvent("\(EventUtils.sessionPreface)sessionDidReconnect", data: [NSNull()]) + let sessionInfo = EventUtils.prepareJSSessionEventData(session); + self.emitEvent("\(session.sessionId):\(EventUtils.sessionPreface)sessionDidReconnect", data: sessionInfo) printLogs("OTRN Session: Session reconnected") } func session(_ session: OTSession, streamCreated stream: OTStream) { OTRN.sharedState.subscriberStreams.updateValue(stream, forKey: stream.streamId) let streamInfo: Dictionary = EventUtils.prepareJSStreamEventData(stream) - self.emitEvent("\(EventUtils.sessionPreface)streamCreated", data: streamInfo) + self.emitEvent("\(session.sessionId):\(EventUtils.sessionPreface)streamCreated", data: streamInfo) setStreamObservers(stream: stream, isPublisherStream: false) printLogs("OTRN: Session streamCreated \(stream.streamId)") } func session(_ session: OTSession, streamDestroyed stream: OTStream) { let streamInfo: Dictionary = EventUtils.prepareJSStreamEventData(stream); - self.emitEvent("\(EventUtils.sessionPreface)streamDestroyed", data: streamInfo) + self.emitEvent("\(session.sessionId):\(EventUtils.sessionPreface)streamDestroyed", data: streamInfo) printLogs("OTRN: Session streamDestroyed: \(stream.streamId)") } func session(_ session: OTSession, didFailWithError error: OTError) { let errorInfo: Dictionary = EventUtils.prepareJSErrorEventData(error); - self.emitEvent("\(EventUtils.sessionPreface)didFailWithError", data: errorInfo) + self.emitEvent("\(session.sessionId):\(EventUtils.sessionPreface)didFailWithError", data: errorInfo) printLogs("OTRN: Session Failed to connect: \(error.localizedDescription)") } @@ -403,7 +444,8 @@ extension OTSessionManager: OTSessionDelegate { signalData["type"] = type; signalData["data"] = string; signalData["connectionId"] = connection?.connectionId; - self.emitEvent("\(EventUtils.sessionPreface)signal", data: signalData) + signalData["sessionId"] = session.sessionId; + self.emitEvent("\(session.sessionId):\(EventUtils.sessionPreface)signal", data: signalData) printLogs("OTRN: Session signal received") } } diff --git a/ios/OpenTokReactNative/OpenTokReactNative-Bridging-Header.h b/ios/OpenTokReactNative/OpenTokReactNative-Bridging-Header.h index feb45aac..ea089522 100644 --- a/ios/OpenTokReactNative/OpenTokReactNative-Bridging-Header.h +++ b/ios/OpenTokReactNative/OpenTokReactNative-Bridging-Header.h @@ -6,3 +6,4 @@ #import #import #import +#import "OTScreenCapture.h" diff --git a/ios/OpenTokReactNative/Utils/EventUtils.swift b/ios/OpenTokReactNative/Utils/EventUtils.swift index d90c8fba..7829306b 100644 --- a/ios/OpenTokReactNative/Utils/EventUtils.swift +++ b/ios/OpenTokReactNative/Utils/EventUtils.swift @@ -25,12 +25,13 @@ class EventUtils { static func prepareJSStreamEventData(_ stream: OTStream) -> Dictionary { var streamInfo: Dictionary = [:]; - guard OTRN.sharedState.session != nil else { return streamInfo } + guard OTRN.sharedState.sessions[stream.session.sessionId] != nil else { return streamInfo } streamInfo["streamId"] = stream.streamId; streamInfo["name"] = stream.name; streamInfo["connectionId"] = stream.connection.connectionId; streamInfo["connection"] = prepareJSConnectionEventData(stream.connection); streamInfo["hasAudio"] = stream.hasAudio; + streamInfo["sessionId"] = stream.session.sessionId; streamInfo["hasVideo"] = stream.hasVideo; streamInfo["creationTime"] = convertDateToString(stream.creationTime); streamInfo["height"] = stream.videoDimensions.height; diff --git a/package-lock.json b/package-lock.json index c40990f0..614075e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "opentok-react-native", - "version": "0.11.2", + "version": "0.12.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 0fe96d83..7af0959c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opentok-react-native", - "version": "0.11.2", + "version": "0.12.0", "description": "React Native components for OpenTok iOS and Android SDKs", "main": "src/index.js", "homepage": "www.tokbox.com", diff --git a/src/OTPublisher.js b/src/OTPublisher.js index cd99c7c6..896ea83e 100644 --- a/src/OTPublisher.js +++ b/src/OTPublisher.js @@ -28,7 +28,7 @@ class OTPublisher extends Component { const publisherEvents = sanitizePublisherEvents(this.state.publisherId, this.props.eventHandlers); setNativeEvents(publisherEvents); OT.setJSComponentEvents(this.componentEventsArray); - this.sessionConnected = nativeEvents.addListener(this.componentEvents.sessionConnected, () => this.sessionConnectedHandler()); + this.sessionConnected = nativeEvents.addListener(`${this.props.sessionId}:${this.componentEvents.sessionConnected}`, () => this.sessionConnectedHandler()); } componentDidMount() { this.createPublisher(); @@ -95,16 +95,18 @@ class OTPublisher extends Component { }); this.otrnEventHandler(initError); } else { - OT.getSessionInfo((session) => { - if (!isNull(session) && isNull(this.state.publisher) && isConnected(session.connectionStatus)) { - this.publish(); - } - }); + if (this.props.sessionId) { + OT.getSessionInfo(this.props.sessionId, (session) => { + if (!isNull(session) && isNull(this.state.publisher) && isConnected(session.connectionStatus)) { + this.publish(); + } + }); + } } }); } publish() { - OT.publish(this.state.publisherId, (publishError) => { + OT.publish(this.props.sessionId, this.state.publisherId, (publishError) => { if (publishError) { this.otrnEventHandler(publishError); } else { diff --git a/src/OTSession.js b/src/OTSession.js index 22259d5c..d448befa 100644 --- a/src/OTSession.js +++ b/src/OTSession.js @@ -20,7 +20,7 @@ export default class OTSession extends Component { const credentials = pick(this.props, ['apiKey', 'sessionId', 'token']); const sanitizedCredentials = sanitizeCredentials(credentials); if (Object.keys(sanitizedCredentials).length === 3) { - const sessionEvents = sanitizeSessionEvents(this.props.eventHandlers); + const sessionEvents = sanitizeSessionEvents(sanitizedCredentials.sessionId, this.props.eventHandlers); const sessionOptions = sanitizeSessionOptions(this.props.options); setNativeEvents(sessionEvents); this.createSession(sanitizedCredentials, sessionOptions); @@ -49,18 +49,19 @@ export default class OTSession extends Component { } createSession(credentials, sessionOptions) { const { signal } = this.props; - OT.initSession(credentials.apiKey, credentials.sessionId, sessionOptions); - OT.connect(credentials.token, (error) => { + const { apiKey, sessionId, token } = credentials; + OT.initSession(apiKey, sessionId, sessionOptions); + OT.connect(sessionId, token, (error) => { if (error) { this.otrnEventHandler(error); } else { - OT.getSessionInfo((session) => { + OT.getSessionInfo(sessionId, (session) => { if (!isNull(session)) { const sessionInfo = { ...session, connectionStatus: getConnectionStatus(session.connectionStatus)}; this.setState({ sessionInfo, }); - logOT(credentials.apiKey, credentials.sessionId, 'rn_on_connect', session.connection.connectionId); + logOT(apiKey, sessionId, 'rn_on_connect', session.connection.connectionId); if (Object.keys(signal).length > 0) { this.signal(signal); } @@ -70,11 +71,11 @@ export default class OTSession extends Component { }); } disconnectSession() { - OT.disconnectSession((disconnectError) => { + OT.disconnectSession(this.props.sessionId, (disconnectError) => { if (disconnectError) { this.otrnEventHandler(disconnectError); } else { - const events = sanitizeSessionEvents(this.props.eventHandlers); + const events = sanitizeSessionEvents(this.props.sessionId, this.props.eventHandlers); removeNativeEvents(events); } }); @@ -84,12 +85,10 @@ export default class OTSession extends Component { } signal(signal) { const signalData = sanitizeSignalData(signal); - OT.sendSignal(signalData.signal, signalData.errorHandler); + OT.sendSignal(this.props.sessionId, signalData.signal, signalData.errorHandler); } render() { - const { style } = this.props; - if (this.props.children) { const childrenWithProps = Children.map( this.props.children, diff --git a/src/OTSubscriber.js b/src/OTSubscriber.js index 973a8954..aa0c24c7 100644 --- a/src/OTSubscriber.js +++ b/src/OTSubscriber.js @@ -22,9 +22,12 @@ export default class OTSubscriber extends Component { this.otrnEventHandler = getOtrnErrorEventHandler(this.props.eventHandlers); } componentWillMount() { - this.streamCreated = nativeEvents.addListener(this.componentEvents.streamCreated, stream => this.streamCreatedHandler(stream)); - this.streamDestroyed = nativeEvents.addListener(this.componentEvents.streamDestroyed, stream => this.streamDestroyedHandler(stream)); - const subscriberEvents = sanitizeSubscriberEvents(this.props.eventHandlers); + const { sessionId, eventHandlers } = this.props; + this.streamCreated = nativeEvents.addListener(`${sessionId}:${this.componentEvents.streamCreated}`, + stream => this.streamCreatedHandler(stream)); + this.streamDestroyed = nativeEvents.addListener(`${sessionId}:${this.componentEvents.streamDestroyed}`, + stream => this.streamDestroyedHandler(stream)); + const subscriberEvents = sanitizeSubscriberEvents(eventHandlers); OT.setJSComponentEvents(this.componentEventsArray); setNativeEvents(subscriberEvents); } @@ -54,16 +57,16 @@ export default class OTSubscriber extends Component { // Subscribe to streams. If subscribeToSelf is true, subscribe also to his own stream const sessionInfoConnectionId = sessionInfo && sessionInfo.connection ? sessionInfo.connection.connectionId : null; if (subscribeToSelf || (sessionInfoConnectionId !== stream.connectionId)){ - OT.subscribeToStream(stream.streamId, subscriberProperties, (error) => { - if (error) { - this.otrnEventHandler(error); - } else { - this.setState({ - streams: [...this.state.streams, stream.streamId], - }); - } - }); + OT.subscribeToStream(stream.streamId, subscriberProperties, (error) => { + if (error) { + this.otrnEventHandler(error); + } else { + this.setState({ + streams: [...this.state.streams, stream.streamId], + }); } + }); + } } streamDestroyedHandler = (stream) => { OT.removeSubscriber(stream.streamId, (error) => { diff --git a/src/helpers/OTHelper.js b/src/helpers/OTHelper.js index 4cd7c9f6..c323dc80 100644 --- a/src/helpers/OTHelper.js +++ b/src/helpers/OTHelper.js @@ -3,13 +3,14 @@ import { handleError } from '../OTError'; import { each } from 'underscore'; import axios from 'axios'; -const reassignEvents = (type, customEvents, events, publisherId) => { +const reassignEvents = (type, customEvents, events, eventKey) => { const newEvents = {}; const preface = `${type}:`; const platform = Platform.OS; + each(events, (eventHandler, eventType) => { - if (customEvents[platform][eventType] !== undefined && publisherId !== undefined) { - newEvents[`${publisherId}:${preface}${customEvents[platform][eventType]}`] = eventHandler; + if (customEvents[platform][eventType] !== undefined && eventKey !== undefined) { + newEvents[`${eventKey}:${preface}${customEvents[platform][eventType]}`] = eventHandler; } else if(customEvents[platform][eventType] !== undefined ) { newEvents[`${preface}${customEvents[platform][eventType]}`] = eventHandler; } else if(events['otrnError']) { @@ -18,6 +19,14 @@ const reassignEvents = (type, customEvents, events, publisherId) => { handleError(`${eventType} is not a supported event`); } }); + + // Set a default handler + each(customEvents[platform], (event) => { + if (eventKey !== undefined && !newEvents[`${eventKey}:${preface}${event}`]) { + newEvents[`${eventKey}:${preface}${event}`] = () => { }; + } + }); + return newEvents; }; diff --git a/src/helpers/OTSessionHelper.js b/src/helpers/OTSessionHelper.js index 343ef447..68ccbd90 100644 --- a/src/helpers/OTSessionHelper.js +++ b/src/helpers/OTSessionHelper.js @@ -7,7 +7,7 @@ const validateString = value => (isString(value) ? value : ''); const validateBoolean = value => (isBoolean(value) ? value : false); -const sanitizeSessionEvents = (events) => { +const sanitizeSessionEvents = (sessionId, events) => { if (typeof events !== 'object') { return {}; } @@ -43,7 +43,7 @@ const sanitizeSessionEvents = (events) => { streamPropertyChanged: 'onStreamPropertyChanged', }, }; - return reassignEvents('session', customEvents, events); + return reassignEvents('session', customEvents, events, sessionId); }; diff --git a/src/views/OTPublisherView.js b/src/views/OTPublisherView.js index cc796da7..dff3229f 100644 --- a/src/views/OTPublisherView.js +++ b/src/views/OTPublisherView.js @@ -9,7 +9,8 @@ class OTPublisherView extends Component { } const viewPropTypes = View.propTypes; OTPublisherView.propTypes = { - publisherId: PropTypes.string.isRequired, + publisherId: PropTypes.string.isRequired, + sessionId: PropTypes.string.isRequired, ...viewPropTypes, };