diff --git a/android/src/main/java/com/twiliorn/library/CustomTwilioVideoView.java b/android/src/main/java/com/twiliorn/library/CustomTwilioVideoView.java index 28effe01..8e231efb 100644 --- a/android/src/main/java/com/twiliorn/library/CustomTwilioVideoView.java +++ b/android/src/main/java/com/twiliorn/library/CustomTwilioVideoView.java @@ -22,8 +22,8 @@ import android.os.Build; import android.os.Handler; import android.os.HandlerThread; -import android.support.annotation.NonNull; -import android.support.annotation.StringDef; +import androidx.annotation.NonNull; +import androidx.annotation.StringDef; import android.util.Log; import android.view.View; @@ -103,6 +103,8 @@ import static com.twiliorn.library.CustomTwilioVideoView.Events.ON_STATS_RECEIVED; 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_RECORDING_STARTED; +import static com.twiliorn.library.CustomTwilioVideoView.Events.ON_RECORDING_STOPPED; public class CustomTwilioVideoView extends View implements LifecycleEventListener, AudioManager.OnAudioFocusChangeListener { private static final String TAG = "CustomTwilioVideoView"; @@ -165,6 +167,8 @@ public class CustomTwilioVideoView extends View implements LifecycleEventListene String ON_STATS_RECEIVED = "onStatsReceived"; String ON_NETWORK_QUALITY_LEVELS_CHANGED = "onNetworkQualityLevelsChanged"; String ON_DOMINANT_SPEAKER_CHANGED = "onDominantSpeakerDidChange"; + String ON_RECORDING_STARTED = "onRecordingStarted"; + String ON_RECORDING_STOPPED = "onRecordingStopped"; } private final ThemedReactContext themedReactContext; @@ -210,6 +214,8 @@ public class CustomTwilioVideoView extends View implements LifecycleEventListene public CustomTwilioVideoView(ThemedReactContext context) { super(context); + releaseResource(); + this.themedReactContext = context; this.eventEmitter = themedReactContext.getJSModule(RCTEventEmitter.class); @@ -382,46 +388,54 @@ public void onHostPause() { @Override public void onHostDestroy() { - /* - * Remove stream voice control - */ - if (themedReactContext.getCurrentActivity() != null) { - themedReactContext.getCurrentActivity().setVolumeControlStream(AudioManager.USE_DEFAULT_STREAM_TYPE); - } - /* - * Always disconnect from the room before leaving the Activity to - * ensure any memory allocated to the Room resource is freed. - */ - if (room != null && room.getState() != Room.State.DISCONNECTED) { - room.disconnect(); - disconnectedFromOnDestroy = true; - } - - /* - * Release the local media ensuring any memory allocated to audio or video is freed. - */ - if (localVideoTrack != null) { - localVideoTrack.release(); - localVideoTrack = null; - } - - if (localAudioTrack != null) { - localAudioTrack.release(); - localAudioTrack = null; - } - - // Quit the data track message thread - dataTrackMessageThread.quit(); - - + releaseResource(); } public void releaseResource() { - themedReactContext.removeLifecycleEventListener(this); - room = null; - localVideoTrack = null; - thumbnailVideoView = null; - cameraCapturer = null; + thumbnailVideoView = null; + roomName = null; + accessToken = null; + + /* + * Remove stream voice control + */ + if (themedReactContext != null && themedReactContext.getCurrentActivity() != null) { + themedReactContext.getCurrentActivity().setVolumeControlStream(AudioManager.USE_DEFAULT_STREAM_TYPE); + themedReactContext.removeLifecycleEventListener(this); + } + /* + * Always disconnect from the room before leaving the Activity to + * ensure any memory allocated to the Room resource is freed. + */ + if (room != null && room.getState() != Room.State.DISCONNECTED) { + room.disconnect(); + disconnectedFromOnDestroy = true; + } + room = null; + + + if (localParticipant != null) { + localParticipant.unpublishTrack(localVideoTrack); + localParticipant = null; + } + + if (localVideoTrack != null) { + localVideoTrack.release(); + localVideoTrack = null; + } + + if (localAudioTrack != null) { + localAudioTrack.release(); + localAudioTrack = null; + } + + if (cameraCapturer != null) { + cameraCapturer.stopCapture(); + cameraCapturer = null; + } + + // Quit the data track message thread + dataTrackMessageThread.quit(); } // ====== CONNECTING =========================================================================== @@ -804,6 +818,14 @@ public void onStats(List statsReports) { } } + public boolean isActive() { + return room != null; + } + + public boolean isRecording() { + return room.isRecording(); + } + public void disableOpenSLES() { WebRtcAudioManager.setBlacklistDeviceForOpenSLESUsage(true); } @@ -915,10 +937,16 @@ public void onParticipantDisconnected(Room room, RemoteParticipant participant) @Override public void onRecordingStarted(Room room) { + WritableMap event = new WritableNativeMap(); + + pushEvent(CustomTwilioVideoView.this, ON_RECORDING_STARTED, event); } @Override public void onRecordingStopped(Room room) { + WritableMap event = new WritableNativeMap(); + + pushEvent(CustomTwilioVideoView.this, ON_RECORDING_STOPPED, event); } @Override diff --git a/android/src/main/java/com/twiliorn/library/CustomTwilioVideoViewManager.java b/android/src/main/java/com/twiliorn/library/CustomTwilioVideoViewManager.java index 40c63b80..192e84f5 100644 --- a/android/src/main/java/com/twiliorn/library/CustomTwilioVideoViewManager.java +++ b/android/src/main/java/com/twiliorn/library/CustomTwilioVideoViewManager.java @@ -8,7 +8,7 @@ */ package com.twiliorn.library; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.common.MapBuilder; @@ -40,6 +40,8 @@ import static com.twiliorn.library.CustomTwilioVideoView.Events.ON_STATS_RECEIVED; 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_RECORDING_STARTED; +import static com.twiliorn.library.CustomTwilioVideoView.Events.ON_RECORDING_STOPPED; public class CustomTwilioVideoViewManager extends SimpleViewManager { @@ -65,6 +67,12 @@ public String getName() { return REACT_CLASS; } + @Override + public void onDropViewInstance(CustomTwilioVideoView view) { + view.onHostDestroy(); + super.onDropViewInstance(view); + } + @Override protected CustomTwilioVideoView createViewInstance(ThemedReactContext reactContext) { return new CustomTwilioVideoView(reactContext); @@ -166,7 +174,9 @@ public Map getExportedCustomDirectEventTypeConstants() { )); map.putAll(MapBuilder.of( - ON_PARTICIPANT_REMOVED_DATA_TRACK, MapBuilder.of("registrationName", ON_PARTICIPANT_REMOVED_DATA_TRACK) + ON_PARTICIPANT_REMOVED_DATA_TRACK, MapBuilder.of("registrationName", ON_PARTICIPANT_REMOVED_DATA_TRACK), + ON_RECORDING_STARTED, MapBuilder.of("registrationName", ON_RECORDING_STARTED), + ON_RECORDING_STOPPED, MapBuilder.of("registrationName", ON_RECORDING_STOPPED) )); map.putAll(MapBuilder.of( @@ -198,4 +208,8 @@ public Map getCommandsMap() { .put("sendString", SEND_STRING) .build(); } + + public boolean isRecording(CustomTwilioVideoView view) { + return view.isRecording(); + } } diff --git a/android/src/main/java/com/twiliorn/library/TwilioPackage.java b/android/src/main/java/com/twiliorn/library/TwilioPackage.java index 05fd7137..58bcbb16 100644 --- a/android/src/main/java/com/twiliorn/library/TwilioPackage.java +++ b/android/src/main/java/com/twiliorn/library/TwilioPackage.java @@ -20,8 +20,10 @@ public class TwilioPackage implements ReactPackage { @Override - public List createNativeModules(ReactApplicationContext reactContext) { - return Collections.emptyList(); + public List createNativeModules(ReactApplicationContext reactApplicationContext) { + return Arrays.asList( + new TwilioVideoModule(reactApplicationContext) + ); } // Deprecated by RN 0.47 diff --git a/android/src/main/java/com/twiliorn/library/TwilioVideoModule.java b/android/src/main/java/com/twiliorn/library/TwilioVideoModule.java new file mode 100644 index 00000000..300e8825 --- /dev/null +++ b/android/src/main/java/com/twiliorn/library/TwilioVideoModule.java @@ -0,0 +1,52 @@ +package com.twiliorn.library; + +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; + +import com.facebook.react.uimanager.UIBlock; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.NativeViewHierarchyManager; + +public class TwilioVideoModule extends ReactContextBaseJavaModule { + private static final String TAG = "TwilioVideoModule"; + + private static ReactApplicationContext mReactContext; + + public TwilioVideoModule(ReactApplicationContext reactContext) { + super(reactContext); + mReactContext = reactContext; + } + + @Override + public String getName() { + return "TwilioVideoModule"; + } + + @ReactMethod + public void isRecording(final int viewTag, final Promise promise) { + final ReactApplicationContext context = getReactApplicationContext(); + UIManagerModule uiManager = context.getNativeModule(UIManagerModule.class); + + uiManager.addUIBlock(new UIBlock() { + @Override + public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) { + final CustomTwilioVideoView videoView; + + try { + videoView = (CustomTwilioVideoView) nativeViewHierarchyManager.resolveView(viewTag); + + if(!videoView.isActive()){ + promise.reject("E_VIEW_UNAVAILABLE", "isRecording: Video is not running"); + } else { + promise.resolve(videoView.isRecording()); + } + } catch (Exception e) { + promise.reject("E_VIEW_NOT_FOUND", "isRecording: Expected a CustomTwilioVideoView component"); + } + } + }); + } +} \ No newline at end of file diff --git a/index.d.ts b/index.d.ts index 2f94f2e3..1ca44311 100644 --- a/index.d.ts +++ b/index.d.ts @@ -108,6 +108,8 @@ declare module "react-native-twilio-video-webrtc" { onStatsReceived?: (data: any) => void; onDataTrackMessageReceived?: DataTrackEventCb; + onRecordingStarted?: () => void; + onRecordingStopped?: () => void; // iOS only autoInitializeCamera?: boolean; ref?: React.Ref; @@ -156,6 +158,7 @@ declare module "react-native-twilio-video-webrtc" { publishLocalVideo: () => void; unpublishLocalVideo: () => void; sendString: (message: string) => void; + isRecording: () => Promise; } class TwilioVideoLocalView extends React.Component< diff --git a/src/TwilioVideo.android.js b/src/TwilioVideo.android.js index b4214e1e..d66d854d 100644 --- a/src/TwilioVideo.android.js +++ b/src/TwilioVideo.android.js @@ -12,140 +12,151 @@ import { UIManager, View, findNodeHandle, - requireNativeComponent -} from 'react-native' -import React, { Component } from 'react' + requireNativeComponent, + NativeModules, +} from "react-native"; +import React, { Component } from "react"; -import PropTypes from 'prop-types' +import PropTypes from "prop-types"; + +const TwilioVideoManager = NativeModules.TwilioVideoModule; const propTypes = { ...View.propTypes, /** - * Callback that is called when camera source changes - */ + * Callback that is called when camera source changes + */ onCameraSwitched: PropTypes.func, /** - * Callback that is called when video is toggled. - */ + * Callback that is called when video is toggled. + */ onVideoChanged: PropTypes.func, /** - * Callback that is called when a audio is toggled. - */ + * Callback that is called when a audio is toggled. + */ onAudioChanged: PropTypes.func, /** - * Callback that is called when user is connected to a room. - */ + * Callback that is called when user is connected to a room. + */ onRoomDidConnect: PropTypes.func, /** - * Callback that is called when connecting to room fails. - */ + * Callback that is called when connecting to room fails. + */ onRoomDidFailToConnect: PropTypes.func, /** - * Callback that is called when user is disconnected from room. - */ + * Callback that is called when user is disconnected from room. + */ onRoomDidDisconnect: PropTypes.func, /** - * Called when a new data track has been added - * - * @param {{participant, track}} - */ + * Called when a new data track has been added + * + * @param {{participant, track}} + */ onParticipantAddedDataTrack: PropTypes.func, /** - * Called when a data track has been removed - * - * @param {{participant, track}} - */ + * Called when a data track has been removed + * + * @param {{participant, track}} + */ onParticipantRemovedDataTrack: PropTypes.func, /** - * Called when an dataTrack receives a message - * - * @param {{message}} - */ + * Called when an dataTrack receives a message + * + * @param {{message}} + */ onDataTrackMessageReceived: PropTypes.func, /** - * Called when a new video track has been added - * - * @param {{participant, track, enabled}} - */ + * Called when a new video track has been added + * + * @param {{participant, track, enabled}} + */ onParticipantAddedVideoTrack: PropTypes.func, /** - * Called when a video track has been removed - * - * @param {{participant, track}} - */ + * Called when a video track has been removed + * + * @param {{participant, track}} + */ onParticipantRemovedVideoTrack: PropTypes.func, /** - * Called when a new audio track has been added - * - * @param {{participant, track}} - */ + * Called when a new audio track has been added + * + * @param {{participant, track}} + */ onParticipantAddedAudioTrack: PropTypes.func, /** - * Called when a audio track has been removed - * - * @param {{participant, track}} - */ + * Called when a audio track has been removed + * + * @param {{participant, track}} + */ onParticipantRemovedAudioTrack: PropTypes.func, /** - * Callback called a participant enters a room. - */ + * Callback called a participant enters a room. + */ onRoomParticipantDidConnect: PropTypes.func, /** - * Callback that is called when a participant exits a room. - */ + * Callback that is called when a participant exits a room. + */ onRoomParticipantDidDisconnect: PropTypes.func, /** - * Called when a video track has been enabled. - * - * @param {{participant, track}} - */ + * Called when a video track has been enabled. + * + * @param {{participant, track}} + */ onParticipantEnabledVideoTrack: PropTypes.func, /** - * Called when a video track has been disabled. - * - * @param {{participant, track}} - */ + * Called when a video track has been disabled. + * + * @param {{participant, track}} + */ onParticipantDisabledVideoTrack: PropTypes.func, /** - * Called when an audio track has been enabled. - * - * @param {{participant, track}} - */ + * Called when an audio track has been enabled. + * + * @param {{participant, track}} + */ onParticipantEnabledAudioTrack: PropTypes.func, /** - * Called when an audio track has been disabled. - * - * @param {{participant, track}} - */ + * Called when an audio track has been disabled. + * + * @param {{participant, track}} + */ onParticipantDisabledAudioTrack: PropTypes.func, /** - * Callback that is called when stats are received (after calling getStats) - */ + * Callback that is called when stats are received (after calling getStats) + */ onStatsReceived: PropTypes.func, /** - * Callback that is called when network quality levels are changed (only if enableNetworkQualityReporting in connect is set to true) - */ + * Callback that is called when network quality levels are changed (only if enableNetworkQualityReporting in connect is set to true) + */ onNetworkQualityLevelsChanged: PropTypes.func, /** - * Called when dominant speaker changes - * @param {{ participant, room }} dominant participant and room - */ - onDominantSpeakerDidChange: PropTypes.func -} + * Called when dominant speaker changes + * @param {{ participant, room }} dominant participant and room + */ + onDominantSpeakerDidChange: PropTypes.func, + /** + * This method is only called when a Room which was not previously recording starts recording. + */ + onRecordingStarted: PropTypes.func, + /** + * This method is only called when a Room which was previously recording stops recording. + */ + onRecordingStopped: PropTypes.func, +}; const nativeEvents = { connectToRoom: 1, @@ -161,20 +172,33 @@ const nativeEvents = { toggleBluetoothHeadset: 11, sendString: 12, publishVideo: 13, - publishAudio: 14 -} + publishAudio: 14, +}; class CustomTwilioVideoView extends Component { - connect ({ + _videoRef = null; + _videoHandle = null; + + setRef = (ref) => { + if (ref) { + this._videoRef = ref; + this._videoHandle = findNodeHandle(ref); + } else { + this._videoRef = null; + this._videoHandle = null; + } + }; + + connect({ roomName, accessToken, - cameraType = 'front', + cameraType = "front", enableAudio = true, enableVideo = true, enableRemoteAudio = true, enableNetworkQualityReporting = false, dominantSpeakerEnabled = false, - maintainVideoTrackInBackground = false + maintainVideoTrackInBackground = false, }) { this.runCommand(nativeEvents.connectToRoom, [ roomName, @@ -185,137 +209,149 @@ class CustomTwilioVideoView extends Component { enableNetworkQualityReporting, dominantSpeakerEnabled, maintainVideoTrackInBackground, - cameraType - ]) + cameraType, + ]); } - sendString (message) { - this.runCommand(nativeEvents.sendString, [ - message - ]) + sendString(message) { + this.runCommand(nativeEvents.sendString, [message]); } - publishLocalAudio () { - this.runCommand(nativeEvents.publishAudio, [true]) + publishLocalAudio() { + this.runCommand(nativeEvents.publishAudio, [true]); } - publishLocalVideo () { - this.runCommand(nativeEvents.publishVideo, [true]) + publishLocalVideo() { + this.runCommand(nativeEvents.publishVideo, [true]); } - unpublishLocalAudio () { - this.runCommand(nativeEvents.publishAudio, [false]) + unpublishLocalAudio() { + this.runCommand(nativeEvents.publishAudio, [false]); } - unpublishLocalVideo () { - this.runCommand(nativeEvents.publishVideo, [false]) + unpublishLocalVideo() { + this.runCommand(nativeEvents.publishVideo, [false]); } - disconnect () { - this.runCommand(nativeEvents.disconnect, []) + disconnect() { + this.runCommand(nativeEvents.disconnect, []); } - componentWillUnmount () { - this.runCommand(nativeEvents.releaseResource, []) + componentWillUnmount() { + this.runCommand(nativeEvents.releaseResource, []); } - flipCamera () { - this.runCommand(nativeEvents.switchCamera, []) + flipCamera() { + this.runCommand(nativeEvents.switchCamera, []); } - setLocalVideoEnabled (enabled) { - this.runCommand(nativeEvents.toggleVideo, [enabled]) - return Promise.resolve(enabled) + setLocalVideoEnabled(enabled) { + this.runCommand(nativeEvents.toggleVideo, [enabled]); + return Promise.resolve(enabled); } - setLocalAudioEnabled (enabled) { - this.runCommand(nativeEvents.toggleSound, [enabled]) - return Promise.resolve(enabled) + setLocalAudioEnabled(enabled) { + this.runCommand(nativeEvents.toggleSound, [enabled]); + return Promise.resolve(enabled); } - setRemoteAudioEnabled (enabled) { - this.runCommand(nativeEvents.toggleRemoteSound, [enabled]) - return Promise.resolve(enabled) + setRemoteAudioEnabled(enabled) { + this.runCommand(nativeEvents.toggleRemoteSound, [enabled]); + return Promise.resolve(enabled); } - setBluetoothHeadsetConnected (enabled) { - this.runCommand(nativeEvents.toggleBluetoothHeadset, [enabled]) - return Promise.resolve(enabled) + setBluetoothHeadsetConnected(enabled) { + this.runCommand(nativeEvents.toggleBluetoothHeadset, [enabled]); + return Promise.resolve(enabled); } - getStats () { - this.runCommand(nativeEvents.getStats, []) + getStats() { + this.runCommand(nativeEvents.getStats, []); } - disableOpenSLES () { - this.runCommand(nativeEvents.disableOpenSLES, []) + disableOpenSLES() { + this.runCommand(nativeEvents.disableOpenSLES, []); } - toggleSoundSetup (speaker) { - this.runCommand(nativeEvents.toggleSoundSetup, [speaker]) + toggleSoundSetup(speaker) { + this.runCommand(nativeEvents.toggleSoundSetup, [speaker]); } - runCommand (event, args) { + runCommand(event, args) { switch (Platform.OS) { - case 'android': + case "android": UIManager.dispatchViewManagerCommand( - findNodeHandle(this.refs.videoView), + findNodeHandle(this._videoHandle), event, args - ) - break + ); + break; default: - break + break; } } - buildNativeEventWrappers () { + buildNativeEventWrappers() { return [ - 'onCameraSwitched', - 'onVideoChanged', - 'onAudioChanged', - 'onRoomDidConnect', - 'onRoomDidFailToConnect', - 'onRoomDidDisconnect', - 'onParticipantAddedDataTrack', - 'onParticipantRemovedDataTrack', - 'onDataTrackMessageReceived', - 'onParticipantAddedVideoTrack', - 'onParticipantRemovedVideoTrack', - 'onParticipantAddedAudioTrack', - 'onParticipantRemovedAudioTrack', - 'onRoomParticipantDidConnect', - 'onRoomParticipantDidDisconnect', - 'onParticipantEnabledVideoTrack', - 'onParticipantDisabledVideoTrack', - 'onParticipantEnabledAudioTrack', - 'onParticipantDisabledAudioTrack', - 'onStatsReceived', - 'onNetworkQualityLevelsChanged', - 'onDominantSpeakerDidChange' + "onCameraSwitched", + "onVideoChanged", + "onAudioChanged", + "onRoomDidConnect", + "onRoomDidFailToConnect", + "onRoomDidDisconnect", + "onParticipantAddedDataTrack", + "onParticipantRemovedDataTrack", + "onDataTrackMessageReceived", + "onParticipantAddedVideoTrack", + "onParticipantRemovedVideoTrack", + "onParticipantAddedAudioTrack", + "onParticipantRemovedAudioTrack", + "onRoomParticipantDidConnect", + "onRoomParticipantDidDisconnect", + "onParticipantEnabledVideoTrack", + "onParticipantDisabledVideoTrack", + "onParticipantEnabledAudioTrack", + "onParticipantDisabledAudioTrack", + "onStatsReceived", + "onNetworkQualityLevelsChanged", + "onDominantSpeakerDidChange", + "onRecordingStarted", + "onRecordingStopped", ].reduce((wrappedEvents, eventName) => { if (this.props[eventName]) { return { ...wrappedEvents, - [eventName]: data => this.props[eventName](data.nativeEvent) - } + [eventName]: (data) => this.props[eventName](data.nativeEvent), + }; } - return wrappedEvents - }, {}) + return wrappedEvents; + }, {}); + } + + isRecording() { + if (!TwilioVideoManager) { + throw new Error("TwilioVideoManager not found"); + } + + return TwilioVideoManager.isRecording(this._videoHandle); } - render () { - return ( - ) + render() { + return ( + + ); } } -CustomTwilioVideoView.propTypes = propTypes +CustomTwilioVideoView.propTypes = propTypes; const NativeCustomTwilioVideoView = requireNativeComponent( - 'RNCustomTwilioVideoView', + "RNCustomTwilioVideoView", CustomTwilioVideoView -) +); -module.exports = CustomTwilioVideoView +module.exports = CustomTwilioVideoView;