diff --git a/android/build.gradle b/android/build.gradle index b345cb57cd..b30a3d8f14 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.3.1' + classpath 'com.android.tools.build:gradle:3.6.3' } } @@ -41,6 +41,6 @@ android { } dependencies { - api 'org.webrtc:google-webrtc:1.0.28262' - implementation "androidx.annotation:annotation:1.0.1" + api 'org.webrtc:google-webrtc:1.0.30039' + implementation "androidx.annotation:annotation:1.1.0" } diff --git a/android/src/main/java/com/cloudwebrtc/webrtc/DataChannelObserver.java b/android/src/main/java/com/cloudwebrtc/webrtc/DataChannelObserver.java index ec98688052..108897fee5 100755 --- a/android/src/main/java/com/cloudwebrtc/webrtc/DataChannelObserver.java +++ b/android/src/main/java/com/cloudwebrtc/webrtc/DataChannelObserver.java @@ -1,30 +1,29 @@ package com.cloudwebrtc.webrtc; -import java.nio.charset.Charset; -import android.util.Base64; +import com.cloudwebrtc.webrtc.utils.AnyThreadSink; +import com.cloudwebrtc.webrtc.utils.ConstraintsMap; import org.webrtc.DataChannel; + +import java.nio.charset.Charset; + +import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.EventChannel; -import com.cloudwebrtc.webrtc.utils.AnyThreadSink; -import com.cloudwebrtc.webrtc.utils.ConstraintsMap; class DataChannelObserver implements DataChannel.Observer, EventChannel.StreamHandler { + private final int mId; private final DataChannel mDataChannel; - private final String peerConnectionId; - private final FlutterWebRTCPlugin plugin; + private EventChannel eventChannel; private EventChannel.EventSink eventSink; - DataChannelObserver(FlutterWebRTCPlugin plugin, String peerConnectionId, int id, DataChannel dataChannel) { - this.peerConnectionId = peerConnectionId; + DataChannelObserver(BinaryMessenger messenger, String peerConnectionId, int id, + DataChannel dataChannel) { mId = id; mDataChannel = dataChannel; - this.plugin = plugin; - this.eventChannel = - new EventChannel( - plugin.registrar().messenger(), - "FlutterWebRTC/dataChannelEvent" + peerConnectionId + String.valueOf(id)); + eventChannel = + new EventChannel(messenger, "FlutterWebRTC/dataChannelEvent" + peerConnectionId + id); eventChannel.setStreamHandler(this); } @@ -53,7 +52,8 @@ public void onCancel(Object o) { } @Override - public void onBufferedAmountChange(long amount) { } + public void onBufferedAmountChange(long amount) { + } @Override public void onStateChange() { @@ -89,8 +89,9 @@ public void onMessage(DataChannel.Buffer buffer) { sendEvent(params); } - void sendEvent(ConstraintsMap params) { - if(eventSink != null) + private void sendEvent(ConstraintsMap params) { + if (eventSink != null) { eventSink.success(params.toMap()); + } } } diff --git a/android/src/main/java/com/cloudwebrtc/webrtc/FlutterWebRTCPlugin.java b/android/src/main/java/com/cloudwebrtc/webrtc/FlutterWebRTCPlugin.java index f4ae90c52d..5351e7f6d2 100644 --- a/android/src/main/java/com/cloudwebrtc/webrtc/FlutterWebRTCPlugin.java +++ b/android/src/main/java/com/cloudwebrtc/webrtc/FlutterWebRTCPlugin.java @@ -2,1373 +2,141 @@ import android.app.Activity; import android.content.Context; -import android.hardware.Camera; -import android.graphics.SurfaceTexture; -import android.media.AudioManager; import android.util.Log; -import android.util.LongSparseArray; - -import com.cloudwebrtc.webrtc.record.AudioChannel; -import com.cloudwebrtc.webrtc.record.FrameCapturer; -import com.cloudwebrtc.webrtc.utils.AnyThreadResult; -import com.cloudwebrtc.webrtc.utils.ConstraintsArray; -import com.cloudwebrtc.webrtc.utils.ConstraintsMap; -import com.cloudwebrtc.webrtc.utils.EglUtils; -import com.cloudwebrtc.webrtc.utils.ObjectType; +import androidx.annotation.NonNull; +import com.cloudwebrtc.webrtc.MethodCallHandlerImpl.AudioManager; import com.cloudwebrtc.webrtc.utils.RTCAudioManager; - -import java.io.UnsupportedEncodingException; -import java.io.File; -import java.nio.ByteBuffer; -import java.util.*; - -import org.webrtc.AudioTrack; -import org.webrtc.DefaultVideoDecoderFactory; -import org.webrtc.DefaultVideoEncoderFactory; -import org.webrtc.EglBase; -import org.webrtc.IceCandidate; -import org.webrtc.Logging; -import org.webrtc.MediaConstraints; -import org.webrtc.MediaStream; -import org.webrtc.MediaStreamTrack; -import org.webrtc.PeerConnection; -import org.webrtc.PeerConnectionFactory; -import org.webrtc.SdpObserver; -import org.webrtc.SessionDescription; -import org.webrtc.VideoTrack; -import org.webrtc.audio.AudioDeviceModule; -import org.webrtc.audio.JavaAudioDeviceModule; - -import io.flutter.plugin.common.EventChannel; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.PluginRegistry.Registrar; import io.flutter.view.TextureRegistry; +import java.util.Set; /** * FlutterWebRTCPlugin */ -public class FlutterWebRTCPlugin implements MethodCallHandler { - - static public final String TAG = "FlutterWebRTCPlugin"; - - private final Registrar registrar; - private final MethodChannel channel; - - public Map localStreams; - public Map localTracks; - private final Map mPeerConnectionObservers; - - private final TextureRegistry textures; - private LongSparseArray renders = new LongSparseArray<>(); - - /** - * The implementation of {@code getUserMedia} extracted into a separate file - * in order to reduce complexity and to (somewhat) separate concerns. - */ - private GetUserMediaImpl getUserMediaImpl; - final PeerConnectionFactory mFactory; - - private AudioDeviceModule audioDeviceModule; - - private RTCAudioManager rtcAudioManager; - - public Activity getActivity() { - return registrar.activity(); - } - - public Context getContext() { - return registrar.context(); - } - - /** - * Plugin registration. - */ - public static void registerWith(Registrar registrar) { - final MethodChannel channel = new MethodChannel(registrar.messenger(), "FlutterWebRTC.Method"); - channel.setMethodCallHandler(new FlutterWebRTCPlugin(registrar, channel)); - } - - public Registrar registrar() { - return this.registrar; - } - - private FlutterWebRTCPlugin(Registrar registrar, MethodChannel channel) { - this.registrar = registrar; - this.channel = channel; - this.textures = registrar.textures(); - mPeerConnectionObservers = new HashMap(); - localStreams = new HashMap(); - localTracks = new HashMap(); - - PeerConnectionFactory.initialize( - PeerConnectionFactory.InitializationOptions.builder(registrar.context()) - .setEnableInternalTracer(true) - .createInitializationOptions()); - - // Initialize EGL contexts required for HW acceleration. - EglBase.Context eglContext = EglUtils.getRootEglBaseContext(); - - getUserMediaImpl = new GetUserMediaImpl(this, registrar.context()); - - audioDeviceModule = JavaAudioDeviceModule.builder(registrar.context()) - .setUseHardwareAcousticEchoCanceler(true) - .setUseHardwareNoiseSuppressor(true) - .setSamplesReadyCallback(getUserMediaImpl.inputSamplesInterceptor) - .createAudioDeviceModule(); - - getUserMediaImpl.audioDeviceModule = (JavaAudioDeviceModule) audioDeviceModule; - - mFactory = PeerConnectionFactory.builder() - .setOptions(new PeerConnectionFactory.Options()) - .setVideoEncoderFactory(new DefaultVideoEncoderFactory(eglContext, false, true)) - .setVideoDecoderFactory(new DefaultVideoDecoderFactory(eglContext)) - .setAudioDeviceModule(audioDeviceModule) - .createPeerConnectionFactory(); - } - - private void startAudioManager() { - if(rtcAudioManager != null) - return; - - rtcAudioManager = RTCAudioManager.create(registrar.context()); - // Store existing audio settings and change audio mode to - // MODE_IN_COMMUNICATION for best possible VoIP performance. - Log.d(TAG, "Starting the audio manager..."); - rtcAudioManager.start(new RTCAudioManager.AudioManagerEvents() { - // This method will be called each time the number of available audio - // devices has changed. - @Override - public void onAudioDeviceChanged( - RTCAudioManager.AudioDevice audioDevice, Set availableAudioDevices) { - onAudioManagerDevicesChanged(audioDevice, availableAudioDevices); - } - }); - } - - private void stopAudioManager() { - if (rtcAudioManager != null) { - Log.d(TAG, "Stoping the audio manager..."); - rtcAudioManager.stop(); - rtcAudioManager = null; - } - } - - // This method is called when the audio manager reports audio device change, - // e.g. from wired headset to speakerphone. - private void onAudioManagerDevicesChanged( - final RTCAudioManager.AudioDevice device, final Set availableDevices) { - Log.d(TAG, "onAudioManagerDevicesChanged: " + availableDevices + ", " - + "selected: " + device); - // TODO(henrika): add callback handler. - } - - @Override - public void onMethodCall(MethodCall call, Result notSafeResult) { - final AnyThreadResult result = new AnyThreadResult(notSafeResult); - if (call.method.equals("createPeerConnection")) { - Map constraints = call.argument("constraints"); - Map configuration = call.argument("configuration"); - String peerConnectionId = peerConnectionInit(new ConstraintsMap(configuration), new ConstraintsMap((constraints))); - ConstraintsMap res = new ConstraintsMap(); - res.putString("peerConnectionId", peerConnectionId); - result.success(res.toMap()); - } else if (call.method.equals("getUserMedia")) { - Map constraints = call.argument("constraints"); - ConstraintsMap constraintsMap = new ConstraintsMap(constraints); - getUserMedia(constraintsMap, result); - } else if (call.method.equals("createLocalMediaStream")) { - createLocalMediaStream(result); - }else if (call.method.equals("getSources")) { - getSources(result); - }else if (call.method.equals("createOffer")) { - String peerConnectionId = call.argument("peerConnectionId"); - Map constraints = call.argument("constraints"); - peerConnectionCreateOffer(peerConnectionId, new ConstraintsMap(constraints), result); - } else if (call.method.equals("createAnswer")) { - String peerConnectionId = call.argument("peerConnectionId"); - Map constraints = call.argument("constraints"); - peerConnectionCreateAnswer(peerConnectionId, new ConstraintsMap(constraints), result); - } else if (call.method.equals("mediaStreamGetTracks")) { - String streamId = call.argument("streamId"); - MediaStream stream = getStreamForId(streamId,""); - Map resultMap = new HashMap<>(); - List audioTracks = new ArrayList<>(); - List videoTracks = new ArrayList<>(); - for (AudioTrack track : stream.audioTracks) { - localTracks.put(track.id(), track); - Map trackMap = new HashMap<>(); - trackMap.put("enabled", track.enabled()); - trackMap.put("id", track.id()); - trackMap.put("kind", track.kind()); - trackMap.put("label", track.id()); - trackMap.put("readyState", "live"); - trackMap.put("remote", false); - audioTracks.add(trackMap); - } - for (VideoTrack track : stream.videoTracks) { - localTracks.put(track.id(), track); - Map trackMap = new HashMap<>(); - trackMap.put("enabled", track.enabled()); - trackMap.put("id", track.id()); - trackMap.put("kind", track.kind()); - trackMap.put("label", track.id()); - trackMap.put("readyState", "live"); - trackMap.put("remote", false); - videoTracks.add(trackMap); - } - resultMap.put("audioTracks", audioTracks); - resultMap.put("videoTracks", videoTracks); - result.success(resultMap); - } else if (call.method.equals("addStream")) { - String streamId = call.argument("streamId"); - String peerConnectionId = call.argument("peerConnectionId"); - peerConnectionAddStream(streamId, peerConnectionId, result); - } else if (call.method.equals("removeStream")) { - String streamId = call.argument("streamId"); - String peerConnectionId = call.argument("peerConnectionId"); - peerConnectionRemoveStream(streamId, peerConnectionId, result); - } else if (call.method.equals("setLocalDescription")) { - String peerConnectionId = call.argument("peerConnectionId"); - Map description = call.argument("description"); - peerConnectionSetLocalDescription(new ConstraintsMap(description), peerConnectionId, result); - } else if (call.method.equals("setRemoteDescription")) { - String peerConnectionId = call.argument("peerConnectionId"); - Map description = call.argument("description"); - peerConnectionSetRemoteDescription(new ConstraintsMap(description), peerConnectionId, result); - } else if (call.method.equals("addCandidate")) { - String peerConnectionId = call.argument("peerConnectionId"); - Map candidate = call.argument("candidate"); - peerConnectionAddICECandidate(new ConstraintsMap(candidate), peerConnectionId, result); - } else if (call.method.equals("getStats")) { - String peerConnectionId = call.argument("peerConnectionId"); - String trackId = call.argument("trackId"); - peerConnectionGetStats(trackId, peerConnectionId, result); - } else if (call.method.equals("createDataChannel")) { - String peerConnectionId = call.argument("peerConnectionId"); - String label = call.argument("label"); - Map dataChannelDict = call.argument("dataChannelDict"); - createDataChannel(peerConnectionId, label, new ConstraintsMap(dataChannelDict), result); - } else if (call.method.equals("dataChannelSend")) { - String peerConnectionId = call.argument("peerConnectionId"); - int dataChannelId = call.argument("dataChannelId"); - String type = call.argument("type"); - Boolean isBinary = type.equals("binary"); - ByteBuffer byteBuffer; - if(isBinary){ - byteBuffer = ByteBuffer.wrap(call.argument("data")); - }else{ - try { - String data = call.argument("data"); - byteBuffer = ByteBuffer.wrap(data.getBytes("UTF-8")); - } catch (UnsupportedEncodingException e) { - Log.d(TAG, "Could not encode text string as UTF-8."); - result.error("dataChannelSendFailed", "Could not encode text string as UTF-8.",null); - return; - } - } - dataChannelSend(peerConnectionId, dataChannelId, byteBuffer, isBinary); - result.success(null); - } else if (call.method.equals("dataChannelClose")) { - String peerConnectionId = call.argument("peerConnectionId"); - int dataChannelId = call.argument("dataChannelId"); - dataChannelClose(peerConnectionId, dataChannelId); - result.success(null); - } else if (call.method.equals("streamDispose")) { - String streamId = call.argument("streamId"); - mediaStreamRelease(streamId); - result.success(null); - }else if (call.method.equals("mediaStreamTrackSetEnable")) { - String trackId = call.argument("trackId"); - Boolean enabled = call.argument("enabled"); - MediaStreamTrack track = getTrackForId(trackId); - if(track != null){ - track.setEnabled(enabled); - } - result.success(null); - }else if (call.method.equals("mediaStreamAddTrack")) { - String streamId = call.argument("streamId"); - String trackId = call.argument("trackId"); - mediaStreamAddTrack(streamId, trackId, result); - }else if (call.method.equals("mediaStreamRemoveTrack")) { - String streamId = call.argument("streamId"); - String trackId = call.argument("trackId"); - mediaStreamRemoveTrack(streamId,trackId, result); - } else if (call.method.equals("trackDispose")) { - String trackId = call.argument("trackId"); - localTracks.remove(trackId); - result.success(null); - } else if (call.method.equals("peerConnectionClose")) { - String peerConnectionId = call.argument("peerConnectionId"); - peerConnectionClose(peerConnectionId); - result.success(null); - } else if(call.method.equals("peerConnectionDispose")){ - String peerConnectionId = call.argument("peerConnectionId"); - peerConnectionDispose(peerConnectionId); - result.success(null); - }else if (call.method.equals("createVideoRenderer")) { - TextureRegistry.SurfaceTextureEntry entry = textures.createSurfaceTexture(); - SurfaceTexture surfaceTexture = entry.surfaceTexture(); - FlutterRTCVideoRenderer render = new FlutterRTCVideoRenderer(surfaceTexture, entry); - renders.put(entry.id(), render); - - EventChannel eventChannel = - new EventChannel( - registrar.messenger(), - "FlutterWebRTC/Texture" + entry.id()); - - eventChannel.setStreamHandler(render); - render.setEventChannel(eventChannel); - render.setId((int)entry.id()); - - ConstraintsMap params = new ConstraintsMap(); - params.putInt("textureId", (int)entry.id()); - result.success(params.toMap()); - } else if (call.method.equals("videoRendererDispose")) { - int textureId = call.argument("textureId"); - FlutterRTCVideoRenderer render = renders.get(textureId); - if(render == null ){ - result.error("FlutterRTCVideoRendererNotFound", "render [" + textureId + "] not found !", null); - return; - } - render.Dispose(); - renders.delete(textureId); - result.success(null); - } else if (call.method.equals("videoRendererSetSrcObject")) { - int textureId = call.argument("textureId"); - String streamId = call.argument("streamId"); - String peerConnectionId = call.argument("ownerTag"); - FlutterRTCVideoRenderer render = renders.get(textureId); - - if (render == null) { - result.error("FlutterRTCVideoRendererNotFound", "render [" + textureId + "] not found !", null); - return; - } - - MediaStream stream = getStreamForId(streamId, peerConnectionId); - render.setStream(stream); - result.success(null); - } else if (call.method.equals("mediaStreamTrackHasTorch")) { - String trackId = call.argument("trackId"); - getUserMediaImpl.hasTorch(trackId, result); - } else if (call.method.equals("mediaStreamTrackSetTorch")) { - String trackId = call.argument("trackId"); - boolean torch = call.argument("torch"); - getUserMediaImpl.setTorch(trackId, torch, result); - } else if (call.method.equals("mediaStreamTrackSwitchCamera")) { - String trackId = call.argument("trackId"); - getUserMediaImpl.switchCamera(trackId, result); - } else if (call.method.equals("setVolume")) { - String trackId = call.argument("trackId"); - double volume = call.argument("volume"); - mediaStreamTrackSetVolume(trackId, volume); - result.success(null); - } else if (call.method.equals("setMicrophoneMute")) { - boolean mute = call.argument("mute"); - rtcAudioManager.setMicrophoneMute(mute); - result.success(null); - } else if (call.method.equals("enableSpeakerphone")) { - boolean enable = call.argument("enable"); - if(rtcAudioManager == null ){ - startAudioManager(); - } - rtcAudioManager.setSpeakerphoneOn(enable); - result.success(null); - } else if(call.method.equals("getDisplayMedia")) { - Map constraints = call.argument("constraints"); - ConstraintsMap constraintsMap = new ConstraintsMap(constraints); - getDisplayMedia(constraintsMap, result); - }else if (call.method.equals("startRecordToFile")) { - //This method can a lot of different exceptions - //so we should notify plugin user about them - try { - String path = call.argument("path"); - VideoTrack videoTrack = null; - String videoTrackId = call.argument("videoTrackId"); - if (videoTrackId != null) { - MediaStreamTrack track = getTrackForId(videoTrackId); - if (track instanceof VideoTrack) - videoTrack = (VideoTrack) track; - } - AudioChannel audioChannel = null; - if (call.hasArgument("audioChannel")) - audioChannel = AudioChannel.values()[(Integer) call.argument("audioChannel")]; - Integer recorderId = call.argument("recorderId"); - if (videoTrack != null || audioChannel != null) { - getUserMediaImpl.startRecordingToFile(path, recorderId, videoTrack, audioChannel); - result.success(null); - } else { - result.error("0", "No tracks", null); - } - } catch (Exception e) { - result.error("-1", e.getMessage(), e); - } - } else if (call.method.equals("stopRecordToFile")) { - Integer recorderId = call.argument("recorderId"); - getUserMediaImpl.stopRecording(recorderId); - result.success(null); - } else if (call.method.equals("captureFrame")) { - String path = call.argument("path"); - String videoTrackId = call.argument("trackId"); - if (videoTrackId != null) { - MediaStreamTrack track = getTrackForId(videoTrackId); - if (track instanceof VideoTrack) - new FrameCapturer((VideoTrack) track, new File(path), result); - else - result.error(null, "It's not video track", null); +public class FlutterWebRTCPlugin implements FlutterPlugin, ActivityAware { + + static public final String TAG = "FlutterWebRTCPlugin"; + + private RTCAudioManager rtcAudioManager; + private MethodChannel channel; + private MethodCallHandlerImpl methodCallHandler; + + public FlutterWebRTCPlugin() { + } + + /** + * Plugin registration. + */ + public static void registerWith(Registrar registrar) { + final FlutterWebRTCPlugin plugin = new FlutterWebRTCPlugin(); + + plugin.startListening(registrar.context(), registrar.messenger(), registrar.textures()); + + if (registrar.activeContext() instanceof Activity) { + plugin.methodCallHandler.setActivity((Activity) registrar.activeContext()); + } + + registrar.addViewDestroyListener(view -> { + plugin.stopListening(); + return false; + }); + } + + @Override + public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { + startListening(binding.getApplicationContext(), binding.getBinaryMessenger(), + binding.getTextureRegistry()); + } + + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + stopListening(); + } + + @Override + public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { + methodCallHandler.setActivity(binding.getActivity()); + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + methodCallHandler.setActivity(null); + } + + @Override + public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { + methodCallHandler.setActivity(binding.getActivity()); + } + + @Override + public void onDetachedFromActivity() { + methodCallHandler.setActivity(null); + } + + private void startListening(final Context context, BinaryMessenger messenger, + TextureRegistry textureRegistry) { + methodCallHandler = new MethodCallHandlerImpl(context, messenger, textureRegistry, + new AudioManager() { + @Override + public void onAudioManagerRequested(boolean requested) { + if (requested) { + if (rtcAudioManager == null) { + rtcAudioManager = RTCAudioManager.create(context); + } + rtcAudioManager.start(FlutterWebRTCPlugin.this::onAudioManagerDevicesChanged); } else { - result.error(null, "Track is null", null); + if (rtcAudioManager != null) { + rtcAudioManager.stop(); + rtcAudioManager = null; + } } - } else if (call.method.equals("getLocalDescription")) { - String peerConnectionId = call.argument("peerConnectionId"); - PeerConnection peerConnection = getPeerConnection(peerConnectionId); - if (peerConnection != null) { - SessionDescription sdp = peerConnection.getLocalDescription(); - ConstraintsMap params = new ConstraintsMap(); - params.putString("sdp", sdp.description); - params.putString("type", sdp.type.canonicalForm()); - result.success(params.toMap()); - } else { - Log.d(TAG, "getLocalDescription() peerConnection is null"); - result.error("getLocalDescriptionFailed", "getLocalDescription() peerConnection is null", null); - } - } else if (call.method.equals("getRemoteDescription")) { - String peerConnectionId = call.argument("peerConnectionId"); - PeerConnection peerConnection = getPeerConnection(peerConnectionId); - if (peerConnection != null) { - SessionDescription sdp = peerConnection.getRemoteDescription(); - ConstraintsMap params = new ConstraintsMap(); - params.putString("sdp", sdp.description); - params.putString("type", sdp.type.canonicalForm()); - result.success(params.toMap()); - } else { - Log.d(TAG, "getRemoteDescription() peerConnection is null"); - result.error("getRemoteDescriptionFailed", "getRemoteDescription() peerConnection is null", null); - } - } else if (call.method.equals("setConfiguration")) { - String peerConnectionId = call.argument("peerConnectionId"); - Map configuration = call.argument("configuration"); - PeerConnection peerConnection = getPeerConnection(peerConnectionId); - if (peerConnection != null) { - peerConnectionSetConfiguration(new ConstraintsMap(configuration), peerConnection); - result.success(null); - } else { - Log.d(TAG, "setConfiguration() peerConnection is null"); - result.error("setConfigurationFailed", "setConfiguration() peerConnection is null", null); - } - } else { - result.notImplemented(); - } - } - - private PeerConnection getPeerConnection(String id) { - PeerConnectionObserver pco = mPeerConnectionObservers.get(id); - return (pco == null) ? null : pco.getPeerConnection(); - } - - private List createIceServers(ConstraintsArray iceServersArray) { - final int size = (iceServersArray == null) ? 0 : iceServersArray.size(); - List iceServers = new ArrayList<>(size); - for (int i = 0; i < size; i++) { - ConstraintsMap iceServerMap = iceServersArray.getMap(i); - boolean hasUsernameAndCredential = iceServerMap.hasKey("username") && iceServerMap.hasKey("credential"); - if (iceServerMap.hasKey("url")) { - if (hasUsernameAndCredential) { - iceServers.add(PeerConnection.IceServer.builder(iceServerMap.getString("url")).setUsername(iceServerMap.getString("username")).setPassword(iceServerMap.getString("credential")).createIceServer()); - } else { - iceServers.add(PeerConnection.IceServer.builder(iceServerMap.getString("url")).createIceServer()); - } - } else if (iceServerMap.hasKey("urls")) { - switch (iceServerMap.getType("urls")) { - case String: - if (hasUsernameAndCredential) { - iceServers.add(PeerConnection.IceServer.builder(iceServerMap.getString("urls")).setUsername(iceServerMap.getString("username")).setPassword(iceServerMap.getString("credential")).createIceServer()); - } else { - iceServers.add(PeerConnection.IceServer.builder(iceServerMap.getString("urls")).createIceServer()); - } - break; - case Array: - ConstraintsArray urls = iceServerMap.getArray("urls"); - List urlsList = new ArrayList<>(); - - for (int j = 0; j < urls.size(); j++) { - urlsList.add(urls.getString(j)); - } - - PeerConnection.IceServer.Builder builder = PeerConnection.IceServer.builder(urlsList); - - if (hasUsernameAndCredential) { - builder - .setUsername(iceServerMap.getString("username")) - .setPassword(iceServerMap.getString("credential")); - } - - iceServers.add(builder.createIceServer()); - - break; - } - } - } - return iceServers; - } - - private PeerConnection.RTCConfiguration parseRTCConfiguration(ConstraintsMap map) { - ConstraintsArray iceServersArray = null; - if (map != null) { - iceServersArray = map.getArray("iceServers"); - } - List iceServers = createIceServers(iceServersArray); - PeerConnection.RTCConfiguration conf = new PeerConnection.RTCConfiguration(iceServers); - if (map == null) { - return conf; - } - - // iceTransportPolicy (public api) - if (map.hasKey("iceTransportPolicy") - && map.getType("iceTransportPolicy") == ObjectType.String) { - final String v = map.getString("iceTransportPolicy"); - if (v != null) { - switch (v) { - case "all": // public - conf.iceTransportsType = PeerConnection.IceTransportsType.ALL; - break; - case "relay": // public - conf.iceTransportsType = PeerConnection.IceTransportsType.RELAY; - break; - case "nohost": - conf.iceTransportsType = PeerConnection.IceTransportsType.NOHOST; - break; - case "none": - conf.iceTransportsType = PeerConnection.IceTransportsType.NONE; - break; - } - } - } - - // bundlePolicy (public api) - if (map.hasKey("bundlePolicy") - && map.getType("bundlePolicy") == ObjectType.String) { - final String v = map.getString("bundlePolicy"); - if (v != null) { - switch (v) { - case "balanced": // public - conf.bundlePolicy = PeerConnection.BundlePolicy.BALANCED; - break; - case "max-compat": // public - conf.bundlePolicy = PeerConnection.BundlePolicy.MAXCOMPAT; - break; - case "max-bundle": // public - conf.bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE; - break; - } - } - } - - // rtcpMuxPolicy (public api) - if (map.hasKey("rtcpMuxPolicy") - && map.getType("rtcpMuxPolicy") == ObjectType.String) { - final String v = map.getString("rtcpMuxPolicy"); - if (v != null) { - switch (v) { - case "negotiate": // public - conf.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.NEGOTIATE; - break; - case "require": // public - conf.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE; - break; - } - } - } - - // FIXME: peerIdentity of type DOMString (public api) - // FIXME: certificates of type sequence (public api) - - // iceCandidatePoolSize of type unsigned short, defaulting to 0 - if (map.hasKey("iceCandidatePoolSize") - && map.getType("iceCandidatePoolSize") == ObjectType.Number) { - final int v = map.getInt("iceCandidatePoolSize"); - if (v > 0) { - conf.iceCandidatePoolSize = v; - } - } - - // sdpSemantics - if (map.hasKey("sdpSemantics") - && map.getType("sdpSemantics") == ObjectType.String) { - final String v = map.getString("sdpSemantics"); - if (v != null) { - switch (v) { - case "plan-b": - conf.sdpSemantics = PeerConnection.SdpSemantics.PLAN_B; - break; - case "unified-plan": - conf.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN; - break; - } - } - } + } - // === below is private api in webrtc === - - // tcpCandidatePolicy (private api) - if (map.hasKey("tcpCandidatePolicy") - && map.getType("tcpCandidatePolicy") == ObjectType.String) { - final String v = map.getString("tcpCandidatePolicy"); - if (v != null) { - switch (v) { - case "enabled": - conf.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.ENABLED; - break; - case "disabled": - conf.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED; - break; - } - } - } - - // candidateNetworkPolicy (private api) - if (map.hasKey("candidateNetworkPolicy") - && map.getType("candidateNetworkPolicy") == ObjectType.String) { - final String v = map.getString("candidateNetworkPolicy"); - if (v != null) { - switch (v) { - case "all": - conf.candidateNetworkPolicy = PeerConnection.CandidateNetworkPolicy.ALL; - break; - case "low_cost": - conf.candidateNetworkPolicy = PeerConnection.CandidateNetworkPolicy.LOW_COST; - break; - } - } - } - - // KeyType (private api) - if (map.hasKey("keyType") - && map.getType("keyType") == ObjectType.String) { - final String v = map.getString("keyType"); - if (v != null) { - switch (v) { - case "RSA": - conf.keyType = PeerConnection.KeyType.RSA; - break; - case "ECDSA": - conf.keyType = PeerConnection.KeyType.ECDSA; - break; - } - } - } - - // continualGatheringPolicy (private api) - if (map.hasKey("continualGatheringPolicy") - && map.getType("continualGatheringPolicy") == ObjectType.String) { - final String v = map.getString("continualGatheringPolicy"); - if (v != null) { - switch (v) { - case "gather_once": - conf.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_ONCE; - break; - case "gather_continually": - conf.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY; - break; - } + @Override + public void setMicrophoneMute(boolean mute) { + if (rtcAudioManager != null) { + rtcAudioManager.setMicrophoneMute(mute); } - } + } - // audioJitterBufferMaxPackets (private api) - if (map.hasKey("audioJitterBufferMaxPackets") - && map.getType("audioJitterBufferMaxPackets") == ObjectType.Number) { - final int v = map.getInt("audioJitterBufferMaxPackets"); - if (v > 0) { - conf.audioJitterBufferMaxPackets = v; + @Override + public void setSpeakerphoneOn(boolean on) { + if (rtcAudioManager != null) { + rtcAudioManager.setSpeakerphoneOn(on); } - } - - // iceConnectionReceivingTimeout (private api) - if (map.hasKey("iceConnectionReceivingTimeout") - && map.getType("iceConnectionReceivingTimeout") == ObjectType.Number) { - final int v = map.getInt("iceConnectionReceivingTimeout"); - conf.iceConnectionReceivingTimeout = v; - } - - // iceBackupCandidatePairPingInterval (private api) - if (map.hasKey("iceBackupCandidatePairPingInterval") - && map.getType("iceBackupCandidatePairPingInterval") == ObjectType.Number) { - final int v = map.getInt("iceBackupCandidatePairPingInterval"); - conf.iceBackupCandidatePairPingInterval = v; - } - - // audioJitterBufferFastAccelerate (private api) - if (map.hasKey("audioJitterBufferFastAccelerate") - && map.getType("audioJitterBufferFastAccelerate") == ObjectType.Boolean) { - final boolean v = map.getBoolean("audioJitterBufferFastAccelerate"); - conf.audioJitterBufferFastAccelerate = v; - } - - // pruneTurnPorts (private api) - if (map.hasKey("pruneTurnPorts") - && map.getType("pruneTurnPorts") == ObjectType.Boolean) { - final boolean v = map.getBoolean("pruneTurnPorts"); - conf.pruneTurnPorts = v; - } - - // presumeWritableWhenFullyRelayed (private api) - if (map.hasKey("presumeWritableWhenFullyRelayed") - && map.getType("presumeWritableWhenFullyRelayed") == ObjectType.Boolean) { - final boolean v = map.getBoolean("presumeWritableWhenFullyRelayed"); - conf.presumeWritableWhenFullyRelayed = v; - } - - return conf; - } - - public String peerConnectionInit( - ConstraintsMap configuration, - ConstraintsMap constraints) { - - String peerConnectionId = getNextStreamUUID(); - PeerConnectionObserver observer = new PeerConnectionObserver(this, peerConnectionId); - PeerConnection peerConnection - = mFactory.createPeerConnection( - parseRTCConfiguration(configuration), - parseMediaConstraints(constraints), - observer); - observer.setPeerConnection(peerConnection); - if(mPeerConnectionObservers.size() == 0) { - startAudioManager(); - } - mPeerConnectionObservers.put(peerConnectionId, observer); - return peerConnectionId; - } - - String getNextStreamUUID() { - String uuid; - - do { - uuid = UUID.randomUUID().toString(); - } while (getStreamForId(uuid,"") != null); - - return uuid; - } - - String getNextTrackUUID() { - String uuid; - - do { - uuid = UUID.randomUUID().toString(); - } while (getTrackForId(uuid) != null); - - return uuid; - } - - MediaStream getStreamForId(String id, String peerConnectionId) { - MediaStream stream = localStreams.get(id); - - if (stream == null) { - if (peerConnectionId.length() > 0) { - PeerConnectionObserver pco = mPeerConnectionObservers.get(peerConnectionId); - stream = pco.remoteStreams.get(id); - } else { - for (Map.Entry entry : mPeerConnectionObservers.entrySet()) { - PeerConnectionObserver pco = entry.getValue(); - stream = pco.remoteStreams.get(id); - if (stream != null) { - break; - } - } - } - } - - return stream; - } - - private MediaStreamTrack getTrackForId(String trackId) { - MediaStreamTrack track = localTracks.get(trackId); - - if (track == null) { - for (Map.Entry entry : mPeerConnectionObservers.entrySet()) { - PeerConnectionObserver pco = entry.getValue(); - track = pco.remoteTracks.get(trackId); - if (track != null) { - break; - } - } - } - - return track; - } - - /** - * Parses a constraint set specified in the form of a JavaScript object into - * a specific List of MediaConstraints.KeyValuePairs. - * - * @param src The constraint set in the form of a JavaScript object to - * parse. - * @param dst The List of MediaConstraints.KeyValuePairs - * into which the specified src is to be parsed. - */ - private void parseConstraints( - ConstraintsMap src, - List dst) { - - for (Map.Entry entry : src.toMap().entrySet()) { - String key = entry.getKey(); - String value = getMapStrValue(src, entry.getKey()); - dst.add(new MediaConstraints.KeyValuePair(key, value)); - } - } - - private String getMapStrValue(ConstraintsMap map, String key) { - if(!map.hasKey(key)){ - return null; - } - ObjectType type = map.getType(key); - switch (type) { - case Boolean: - return String.valueOf(map.getBoolean(key)); - case Number: - // Don't know how to distinguish between Int and Double from - // ReadableType.Number. 'getInt' will fail on double value, - // while 'getDouble' works for both. - // return String.valueOf(map.getInt(key)); - return String.valueOf(map.getDouble(key)); - case String: - return map.getString(key); - default: - return null; - } - } - - /** - * Parses mandatory and optional "GUM" constraints described by a specific - * ConstraintsMap. - * - * @param constraints A ConstraintsMap which represents a JavaScript - * object specifying the constraints to be parsed into a - * MediaConstraints instance. - * @return A new MediaConstraints instance initialized with the - * mandatory and optional constraint keys and values specified by - * constraints. - */ - MediaConstraints parseMediaConstraints(ConstraintsMap constraints) { - MediaConstraints mediaConstraints = new MediaConstraints(); - - if (constraints.hasKey("mandatory") - && constraints.getType("mandatory") == ObjectType.Map) { - parseConstraints(constraints.getMap("mandatory"), - mediaConstraints.mandatory); - } else { - Log.d(TAG, "mandatory constraints are not a map"); - } - - if (constraints.hasKey("optional") - && constraints.getType("optional") == ObjectType.Array) { - ConstraintsArray optional = constraints.getArray("optional"); - - for (int i = 0, size = optional.size(); i < size; i++) { - if (optional.getType(i) == ObjectType.Map) { - parseConstraints( - optional.getMap(i), - mediaConstraints.optional); - } - } - } else { - Log.d(TAG, "optional constraints are not an array"); - } - - return mediaConstraints; - } - - public void getUserMedia(ConstraintsMap constraints, Result result) { - String streamId = getNextStreamUUID(); - MediaStream mediaStream = mFactory.createLocalMediaStream(streamId); - - if (mediaStream == null) { - // XXX The following does not follow the getUserMedia() algorithm - // specified by - // https://www.w3.org/TR/mediacapture-streams/#dom-mediadevices-getusermedia - // with respect to distinguishing the various causes of failure. - result.error( - /* type */ "getUserMediaFailed", - "Failed to create new media stream", null); - return; - } - - getUserMediaImpl.getUserMedia(constraints, result, mediaStream); - } - - public void getDisplayMedia(ConstraintsMap constraints, Result result) { - String streamId = getNextStreamUUID(); - MediaStream mediaStream = mFactory.createLocalMediaStream(streamId); - - if (mediaStream == null) { - // XXX The following does not follow the getUserMedia() algorithm - // specified by - // https://www.w3.org/TR/mediacapture-streams/#dom-mediadevices-getusermedia - // with respect to distinguishing the various causes of failure. - result.error( - /* type */ "getDisplayMedia", - "Failed to create new media stream", null); - return; - } - - getUserMediaImpl.getDisplayMedia(constraints, result, mediaStream); - } - - public void getSources(Result result) { - ConstraintsArray array = new ConstraintsArray(); - String[] names = new String[Camera.getNumberOfCameras()]; - - for (int i = 0; i < Camera.getNumberOfCameras(); ++i) { - ConstraintsMap info = getCameraInfo(i); - if (info != null) { - array.pushMap(info); - } - } - - ConstraintsMap audio = new ConstraintsMap(); - audio.putString("label", "Audio"); - audio.putString("deviceId", "audio-1"); - audio.putString("facing", ""); - audio.putString("kind", "audioinput"); - array.pushMap(audio); - result.success(array); - } - - private void createLocalMediaStream(Result result) { - String streamId = getNextStreamUUID(); - MediaStream mediaStream = mFactory.createLocalMediaStream(streamId); - localStreams.put(streamId, mediaStream); - - if (mediaStream == null) { - result.error(/* type */ "createLocalMediaStream", "Failed to create new media stream", null); - return; - } - Map resultMap = new HashMap<>(); - resultMap.put("streamId", mediaStream.getId()); - result.success(resultMap); - } - - public void mediaStreamTrackStop(final String id) { - // Is this functionality equivalent to `mediaStreamTrackRelease()` ? - // if so, we should merge this two and remove track from stream as well. - MediaStreamTrack track = localTracks.get(id); - if (track == null) { - Log.d(TAG, "mediaStreamTrackStop() track is null"); - return; - } - track.setEnabled(false); - if (track.kind().equals("video")) { - getUserMediaImpl.removeVideoCapturer(id); - } - localTracks.remove(id); - // What exactly does `detached` mean in doc? - // see: https://www.w3.org/TR/mediacapture-streams/#track-detached - } - - public void mediaStreamTrackSetEnabled(final String id, final boolean enabled) { - MediaStreamTrack track = localTracks.get(id); - if (track == null) { - Log.d(TAG, "mediaStreamTrackSetEnabled() track is null"); - return; - } else if (track.enabled() == enabled) { - return; - } - track.setEnabled(enabled); - } - - public void mediaStreamTrackSetVolume(final String id, final double volume) { - MediaStreamTrack track = localTracks.get(id); - if (track != null && track instanceof AudioTrack) { - Log.d(TAG, "setVolume(): " + id + "," + volume); - try { - ((AudioTrack)track).setVolume(volume); - } catch (Exception e) { - Log.e(TAG, "setVolume(): error", e); - } - } else { - Log.w(TAG, "setVolume(): track not found: " + id); - } - } - - public void mediaStreamAddTrack(final String streaemId, final String trackId, Result result) { - MediaStream mediaStream = localStreams.get(streaemId); - if (mediaStream != null) { - MediaStreamTrack track = localTracks.get(trackId); - if (track != null){ - if (track.kind().equals("audio")) { - mediaStream.addTrack((AudioTrack) track); - } else if (track.kind().equals("video")) { - mediaStream.addTrack((VideoTrack) track); - } - } else { - String errorMsg = "mediaStreamAddTrack() track [" + trackId + "] is null"; - Log.d(TAG, errorMsg); - result.error("mediaStreamAddTrack", errorMsg, null); - } - } else { - String errorMsg = "mediaStreamAddTrack() stream [" + trackId + "] is null"; - Log.d(TAG, errorMsg); - result.error("mediaStreamAddTrack", errorMsg, null); - } - result.success(null); - } - - public void mediaStreamRemoveTrack(final String streaemId, final String trackId, Result result) { - MediaStream mediaStream = localStreams.get(streaemId); - if (mediaStream != null) { - MediaStreamTrack track = localTracks.get(trackId); - if (track != null) { - if (track.kind().equals("audio")) { - mediaStream.removeTrack((AudioTrack) track); - } else if (track.kind().equals("video")) { - mediaStream.removeTrack((VideoTrack) track); - } - } else { - String errorMsg = "mediaStreamRemoveTrack() track [" + trackId + "] is null"; - Log.d(TAG, errorMsg); - result.error("mediaStreamRemoveTrack", errorMsg, null); - } - } else { - String errorMsg = "mediaStreamRemoveTrack() stream [" + trackId + "] is null"; - Log.d(TAG, errorMsg); - result.error("mediaStreamRemoveTrack", errorMsg, null); - } - result.success(null); - } - - public void mediaStreamTrackRelease(final String streamId, final String _trackId) { - MediaStream stream = localStreams.get(streamId); - if (stream == null) { - Log.d(TAG, "mediaStreamTrackRelease() stream is null"); - return; - } - MediaStreamTrack track = localTracks.get(_trackId); - if (track == null) { - Log.d(TAG, "mediaStreamTrackRelease() track is null"); - return; - } - track.setEnabled(false); // should we do this? - localTracks.remove(_trackId); - if (track.kind().equals("audio")) { - stream.removeTrack((AudioTrack) track); - } else if (track.kind().equals("video")) { - stream.removeTrack((VideoTrack) track); - getUserMediaImpl.removeVideoCapturer(_trackId); - } - } - - public ConstraintsMap getCameraInfo(int index) { - Camera.CameraInfo info = new Camera.CameraInfo(); - - try { - Camera.getCameraInfo(index, info); - } catch (Exception e) { - Logging.e("CameraEnumerationAndroid", "getCameraInfo failed on index " + index, e); - return null; - } - ConstraintsMap params = new ConstraintsMap(); - String facing = info.facing == 1 ? "front" : "back"; - params.putString("label", "Camera " + index + ", Facing " + facing + ", Orientation " + info.orientation); - params.putString("deviceId", "" + index); - params.putString("facing", facing); - params.putString("kind", "videoinput"); - return params; - } - - private MediaConstraints defaultConstraints() { - MediaConstraints constraints = new MediaConstraints(); - // TODO video media - constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true")); - constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true")); - constraints.optional.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true")); - return constraints; - } - - public void peerConnectionSetConfiguration(ConstraintsMap configuration, PeerConnection peerConnection) { - if (peerConnection == null) { - Log.d(TAG, "peerConnectionSetConfiguration() peerConnection is null"); - return; - } - peerConnection.setConfiguration(parseRTCConfiguration(configuration)); - } - - public void peerConnectionAddStream(final String streamId, final String id, Result result) { - MediaStream mediaStream = localStreams.get(streamId); - if (mediaStream == null) { - Log.d(TAG, "peerConnectionAddStream() mediaStream is null"); - return; - } - PeerConnection peerConnection = getPeerConnection(id); - if (peerConnection != null) { - boolean res = peerConnection.addStream(mediaStream); - Log.d(TAG, "addStream" + result); - result.success(res); - } else { - Log.d(TAG, "peerConnectionAddStream() peerConnection is null"); - result.error("peerConnectionAddStreamFailed", "peerConnectionAddStream() peerConnection is null", null); - } - } - - public void peerConnectionRemoveStream(final String streamId, final String id, Result result) { - MediaStream mediaStream = localStreams.get(streamId); - if (mediaStream == null) { - Log.d(TAG, "peerConnectionRemoveStream() mediaStream is null"); - return; - } - PeerConnection peerConnection = getPeerConnection(id); - if (peerConnection != null) { - peerConnection.removeStream(mediaStream); - result.success(null); - } else { - Log.d(TAG, "peerConnectionRemoveStream() peerConnection is null"); - result.error("peerConnectionRemoveStreamFailed", "peerConnectionAddStream() peerConnection is null", null); - } - } - - public void peerConnectionCreateOffer( - String id, - ConstraintsMap constraints, - final Result result) { - PeerConnection peerConnection = getPeerConnection(id); - - if (peerConnection != null) { - peerConnection.createOffer(new SdpObserver() { - @Override - public void onCreateFailure(String s) { - result.error("WEBRTC_CREATE_OFFER_ERROR", s, null); - } - - @Override - public void onCreateSuccess(final SessionDescription sdp) { - ConstraintsMap params = new ConstraintsMap(); - params.putString("sdp", sdp.description); - params.putString("type", sdp.type.canonicalForm()); - result.success(params.toMap()); - } - - @Override - public void onSetFailure(String s) { - } - - @Override - public void onSetSuccess() { - } - }, parseMediaConstraints(constraints)); - } else { - Log.d(TAG, "peerConnectionCreateOffer() peerConnection is null"); - result.error("WEBRTC_CREATE_OFFER_ERROR", "peerConnection is null", null); - } - } - - public void peerConnectionCreateAnswer( - String id, - ConstraintsMap constraints, - final Result result) { - PeerConnection peerConnection = getPeerConnection(id); - - if (peerConnection != null) { - peerConnection.createAnswer(new SdpObserver() { - @Override - public void onCreateFailure(String s) { - result.error("WEBRTC_CREATE_ANSWER_ERROR", s, null); - } - - @Override - public void onCreateSuccess(final SessionDescription sdp) { - ConstraintsMap params = new ConstraintsMap(); - params.putString("sdp", sdp.description); - params.putString("type", sdp.type.canonicalForm()); - result.success(params.toMap()); - } - - @Override - public void onSetFailure(String s) { - } - - @Override - public void onSetSuccess() { - } - }, parseMediaConstraints(constraints)); - } else { - Log.d(TAG, "peerConnectionCreateAnswer() peerConnection is null"); - result.error("WEBRTC_CREATE_ANSWER_ERROR", "peerConnection is null", null); - } - } - - public void peerConnectionSetLocalDescription(ConstraintsMap sdpMap, final String id, final Result result) { - PeerConnection peerConnection = getPeerConnection(id); - - Log.d(TAG, "peerConnectionSetLocalDescription() start"); - if (peerConnection != null) { - SessionDescription sdp = new SessionDescription( - SessionDescription.Type.fromCanonicalForm(sdpMap.getString("type")), - sdpMap.getString("sdp") - ); - - peerConnection.setLocalDescription(new SdpObserver() { - @Override - public void onCreateSuccess(final SessionDescription sdp) { - } - - @Override - public void onSetSuccess() { - result.success(null); - } - - @Override - public void onCreateFailure(String s) { - } - - @Override - public void onSetFailure(String s) { - result.error("WEBRTC_SET_LOCAL_DESCRIPTION_ERROR", s, null); - } - }, sdp); - } else { - Log.d(TAG, "peerConnectionSetLocalDescription() peerConnection is null"); - result.error("WEBRTC_SET_LOCAL_DESCRIPTION_ERROR", "peerConnection is null", null); - } - Log.d(TAG, "peerConnectionSetLocalDescription() end"); - } - - public void peerConnectionSetRemoteDescription(final ConstraintsMap sdpMap, final String id, final Result result) { - PeerConnection peerConnection = getPeerConnection(id); - // final String d = sdpMap.getString("type"); - - Log.d(TAG, "peerConnectionSetRemoteDescription() start"); - if (peerConnection != null) { - SessionDescription sdp = new SessionDescription( - SessionDescription.Type.fromCanonicalForm(sdpMap.getString("type")), - sdpMap.getString("sdp") - ); - - peerConnection.setRemoteDescription(new SdpObserver() { - @Override - public void onCreateSuccess(final SessionDescription sdp) { - } - - @Override - public void onSetSuccess() { - result.success(null); - } - - @Override - public void onCreateFailure(String s) { - } - - @Override - public void onSetFailure(String s) { - result.error("WEBRTC_SET_REMOTE_DESCRIPTION_ERROR", s, null); - } - }, sdp); - } else { - Log.d(TAG, "peerConnectionSetRemoteDescription() peerConnection is null"); - result.error("WEBRTC_SET_REMOTE_DESCRIPTION_ERROR", "peerConnection is null", null); - } - Log.d(TAG, "peerConnectionSetRemoteDescription() end"); - } + } + }); - public void peerConnectionAddICECandidate(ConstraintsMap candidateMap, final String id, final Result result) { - boolean res = false; - PeerConnection peerConnection = getPeerConnection(id); - Log.d(TAG, "peerConnectionAddICECandidate() start"); - if (peerConnection != null) { - IceCandidate candidate = new IceCandidate( - candidateMap.getString("sdpMid"), - candidateMap.getInt("sdpMLineIndex"), - candidateMap.getString("candidate") - ); - res = peerConnection.addIceCandidate(candidate); - } else { - Log.d(TAG, "peerConnectionAddICECandidate() peerConnection is null"); - result.error("peerConnectionAddICECandidateFailed", "peerConnectionAddICECandidate() peerConnection is null", null); - } - result.success(res); - Log.d(TAG, "peerConnectionAddICECandidate() end"); - } + channel = new MethodChannel(messenger, "FlutterWebRTC.Method"); + channel.setMethodCallHandler(methodCallHandler); + } - public void peerConnectionGetStats(String trackId, String id, final Result result) { - PeerConnectionObserver pco = mPeerConnectionObservers.get(id); - if (pco == null || pco.getPeerConnection() == null) { - Log.d(TAG, "peerConnectionGetStats() peerConnection is null"); - } else { - pco.getStats(trackId, result); - } - } + private void stopListening() { + methodCallHandler.dispose(); + methodCallHandler = null; + channel.setMethodCallHandler(null); - public void peerConnectionClose(final String id) { - PeerConnectionObserver pco = mPeerConnectionObservers.get(id); - if (pco == null || pco.getPeerConnection() == null) { - Log.d(TAG, "peerConnectionClose() peerConnection is null"); - } else { - pco.close(); - } - } - public void peerConnectionDispose(final String id) { - PeerConnectionObserver pco = mPeerConnectionObservers.get(id); - if (pco == null || pco.getPeerConnection() == null) { - Log.d(TAG, "peerConnectionDispose() peerConnection is null"); - } else { - pco.dispose(); - mPeerConnectionObservers.remove(id); - } - if(mPeerConnectionObservers.size() == 0) { - stopAudioManager(); - } + if (rtcAudioManager != null) { + Log.d(TAG, "Stopping the audio manager..."); + rtcAudioManager.stop(); + rtcAudioManager = null; } + } - public void mediaStreamRelease(final String id) { - MediaStream mediaStream = localStreams.get(id); - if (mediaStream != null) { - for (VideoTrack track : mediaStream.videoTracks) { - localTracks.remove(track.id()); - getUserMediaImpl.removeVideoCapturer(track.id()); - } - for (AudioTrack track : mediaStream.audioTracks) { - localTracks.remove(track.id()); - } - localStreams.remove(id); - } else { - Log.d(TAG, "mediaStreamRelease() mediaStream is null"); - } - } - - public void createDataChannel(final String peerConnectionId, String label, ConstraintsMap config, Result result) { - // Forward to PeerConnectionObserver which deals with DataChannels - // because DataChannel is owned by PeerConnection. - PeerConnectionObserver pco - = mPeerConnectionObservers.get(peerConnectionId); - if (pco == null || pco.getPeerConnection() == null) { - Log.d(TAG, "createDataChannel() peerConnection is null"); - } else { - pco.createDataChannel(label, config, result); - } - } + // This method is called when the audio manager reports audio device change, + // e.g. from wired headset to speakerphone. + private void onAudioManagerDevicesChanged( + final RTCAudioManager.AudioDevice device, + final Set availableDevices) { + Log.d(TAG, "onAudioManagerDevicesChanged: " + availableDevices + ", " + + "selected: " + device); + // TODO(henrika): add callback handler. + } - public void dataChannelSend(String peerConnectionId, int dataChannelId, ByteBuffer bytebuffer, Boolean isBinary) { - // Forward to PeerConnectionObserver which deals with DataChannels - // because DataChannel is owned by PeerConnection. - PeerConnectionObserver pco - = mPeerConnectionObservers.get(peerConnectionId); - if (pco == null || pco.getPeerConnection() == null) { - Log.d(TAG, "dataChannelSend() peerConnection is null"); - } else { - pco.dataChannelSend(dataChannelId, bytebuffer, isBinary); - } - } - - public void dataChannelClose(String peerConnectionId, int dataChannelId) { - // Forward to PeerConnectionObserver which deals with DataChannels - // because DataChannel is owned by PeerConnection. - PeerConnectionObserver pco - = mPeerConnectionObservers.get(peerConnectionId); - if (pco == null || pco.getPeerConnection() == null) { - Log.d(TAG, "dataChannelClose() peerConnection is null"); - } else { - pco.dataChannelClose(dataChannelId); - } - } } diff --git a/android/src/main/java/com/cloudwebrtc/webrtc/GetUserMediaImpl.java b/android/src/main/java/com/cloudwebrtc/webrtc/GetUserMediaImpl.java index fb38ae4f84..8d94262ec5 100755 --- a/android/src/main/java/com/cloudwebrtc/webrtc/GetUserMediaImpl.java +++ b/android/src/main/java/com/cloudwebrtc/webrtc/GetUserMediaImpl.java @@ -1,34 +1,39 @@ package com.cloudwebrtc.webrtc; import android.Manifest; +import android.app.Activity; import android.app.Fragment; import android.app.FragmentTransaction; import android.content.ContentValues; import android.content.Context; +import android.content.Intent; import android.content.pm.PackageManager; import android.hardware.Camera; +import android.hardware.Camera.Parameters; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraManager; import android.hardware.camera2.CaptureRequest; +import android.media.projection.MediaProjection; +import android.media.projection.MediaProjectionManager; import android.os.Build; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.ResultReceiver; import android.provider.MediaStore; -import androidx.annotation.Nullable; import android.util.Log; -import android.content.Intent; -import android.app.Activity; -import android.view.Surface; -import android.view.WindowManager; -import android.media.projection.MediaProjection; -import android.media.projection.MediaProjectionManager; import android.util.Range; import android.util.SparseArray; +import android.view.Surface; +import android.view.WindowManager; + +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.cloudwebrtc.webrtc.record.AudioChannel; import com.cloudwebrtc.webrtc.record.AudioSamplesInterceptor; @@ -38,9 +43,30 @@ import com.cloudwebrtc.webrtc.utils.ConstraintsArray; import com.cloudwebrtc.webrtc.utils.ConstraintsMap; import com.cloudwebrtc.webrtc.utils.EglUtils; +import com.cloudwebrtc.webrtc.utils.MediaConstraintsUtils; import com.cloudwebrtc.webrtc.utils.ObjectType; import com.cloudwebrtc.webrtc.utils.PermissionUtils; +import org.webrtc.AudioSource; +import org.webrtc.AudioTrack; +import org.webrtc.Camera1Capturer; +import org.webrtc.Camera1Enumerator; +import org.webrtc.Camera2Capturer; +import org.webrtc.Camera2Enumerator; +import org.webrtc.CameraEnumerationAndroid.CaptureFormat; +import org.webrtc.CameraEnumerator; +import org.webrtc.CameraVideoCapturer; +import org.webrtc.MediaConstraints; +import org.webrtc.MediaStream; +import org.webrtc.MediaStreamTrack; +import org.webrtc.PeerConnectionFactory; +import org.webrtc.ScreenCapturerAndroid; +import org.webrtc.SurfaceTextureHelper; +import org.webrtc.VideoCapturer; +import org.webrtc.VideoSource; +import org.webrtc.VideoTrack; +import org.webrtc.audio.JavaAudioDeviceModule; + import java.io.File; import java.lang.reflect.Field; import java.util.ArrayList; @@ -48,20 +74,17 @@ import java.util.List; import java.util.Map; -import org.webrtc.*; -import org.webrtc.audio.JavaAudioDeviceModule; -import org.webrtc.CameraEnumerationAndroid.CaptureFormat; - import io.flutter.plugin.common.MethodChannel.Result; /** - * The implementation of {@code getUserMedia} extracted into a separate file in - * order to reduce complexity and to (somewhat) separate concerns. + * The implementation of {@code getUserMedia} extracted into a separate file in order to reduce + * complexity and to (somewhat) separate concerns. */ -class GetUserMediaImpl{ - private static final int DEFAULT_WIDTH = 1280; +class GetUserMediaImpl { + + private static final int DEFAULT_WIDTH = 1280; private static final int DEFAULT_HEIGHT = 720; - private static final int DEFAULT_FPS = 30; + private static final int DEFAULT_FPS = 30; private static final String PERMISSION_AUDIO = Manifest.permission.RECORD_AUDIO; private static final String PERMISSION_VIDEO = Manifest.permission.CAMERA; @@ -75,11 +98,10 @@ class GetUserMediaImpl{ static final String TAG = FlutterWebRTCPlugin.TAG; - private final Map mVideoCapturers - = new HashMap(); + private final Map mVideoCapturers = new HashMap<>(); + private final StateProvider stateProvider; private final Context applicationContext; - private final FlutterWebRTCPlugin plugin; static final int minAPILevel = Build.VERSION_CODES.LOLLIPOP; private MediaProjectionManager mProjectionManager = null; @@ -90,8 +112,12 @@ class GetUserMediaImpl{ JavaAudioDeviceModule audioDeviceModule; private final SparseArray mediaRecorders = new SparseArray<>(); - public void screenRequestPremissions(ResultReceiver resultReceiver){ - Activity activity = plugin.getActivity(); + public void screenRequestPremissions(ResultReceiver resultReceiver) { + final Activity activity = stateProvider.getActivity(); + if (activity == null) { + // Activity went away, nothing we can do. + return; + } Bundle args = new Bundle(); args.putParcelable(RESULT_RECEIVER, resultReceiver); @@ -100,10 +126,11 @@ public void screenRequestPremissions(ResultReceiver resultReceiver){ ScreenRequestPermissionsFragment fragment = new ScreenRequestPermissionsFragment(); fragment.setArguments(args); - FragmentTransaction transaction - = activity.getFragmentManager().beginTransaction().add( - fragment, - fragment.getClass().getName()); + FragmentTransaction transaction = + activity + .getFragmentManager() + .beginTransaction() + .add(fragment, fragment.getClass().getName()); try { transaction.commit(); @@ -114,12 +141,12 @@ public void screenRequestPremissions(ResultReceiver resultReceiver){ public static class ScreenRequestPermissionsFragment extends Fragment { - private ResultReceiver resultReceiver = null; - private int requestCode = 0; + private ResultReceiver resultReceiver = null; + private int requestCode = 0; private int resultCode = 0; private void checkSelfPermissions(boolean requestPermissions) { - if(resultCode != Activity.RESULT_OK) { + if (resultCode != Activity.RESULT_OK) { Activity activity = this.getActivity(); Bundle args = getArguments(); resultReceiver = args.getParcelable(RESULT_RECEIVER); @@ -130,12 +157,13 @@ private void checkSelfPermissions(boolean requestPermissions) { public void requestStart(Activity activity, int requestCode) { if (android.os.Build.VERSION.SDK_INT < minAPILevel) { - Log.w(TAG, "Can't run requestStart() due to a low API level. API level 21 or higher is required."); + Log.w( + TAG, + "Can't run requestStart() due to a low API level. API level 21 or higher is required."); return; } else { MediaProjectionManager mediaProjectionManager = - (MediaProjectionManager) activity.getSystemService( - Context.MEDIA_PROJECTION_SERVICE); + (MediaProjectionManager) activity.getSystemService(Context.MEDIA_PROJECTION_SERVICE); // call for the projection manager this.startActivityForResult( @@ -143,7 +171,6 @@ public void requestStart(Activity activity, int requestCode) { } } - @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); @@ -168,9 +195,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { private void finish() { Activity activity = getActivity(); if (activity != null) { - activity.getFragmentManager().beginTransaction() - .remove(this) - .commitAllowingStateLoss(); + activity.getFragmentManager().beginTransaction().remove(this).commitAllowingStateLoss(); } } @@ -181,46 +206,42 @@ public void onResume() { } } - GetUserMediaImpl( - FlutterWebRTCPlugin plugin, - Context applicationContext) { - this.plugin = plugin; - this.applicationContext = applicationContext; + GetUserMediaImpl(StateProvider stateProvider, Context applicationContext) { + this.stateProvider = stateProvider; + this.applicationContext = applicationContext; } /** * Includes default constraints set for the audio media type. - * @param audioConstraints MediaConstraints instance to be filled - * with the default constraints for audio media type. + * + * @param audioConstraints MediaConstraints instance to be filled with the default + * constraints for audio media type. */ private void addDefaultAudioConstraints(MediaConstraints audioConstraints) { audioConstraints.optional.add( - new MediaConstraints.KeyValuePair("googNoiseSuppression", "true")); - audioConstraints.optional.add( - new MediaConstraints.KeyValuePair("googEchoCancellation", "true")); + new MediaConstraints.KeyValuePair("googNoiseSuppression", "true")); audioConstraints.optional.add( - new MediaConstraints.KeyValuePair("echoCancellation", "true")); + new MediaConstraints.KeyValuePair("googEchoCancellation", "true")); + audioConstraints.optional.add(new MediaConstraints.KeyValuePair("echoCancellation", "true")); audioConstraints.optional.add( - new MediaConstraints.KeyValuePair("googEchoCancellation2", "true")); + new MediaConstraints.KeyValuePair("googEchoCancellation2", "true")); audioConstraints.optional.add( - new MediaConstraints.KeyValuePair( - "googDAEchoCancellation", "true")); + new MediaConstraints.KeyValuePair("googDAEchoCancellation", "true")); } /** * Create video capturer via given facing mode - * @param enumerator a CameraEnumerator provided by webrtc - * it can be Camera1Enumerator or Camera2Enumerator - * @param isFacing 'user' mapped with 'front' is true (default) - * 'environment' mapped with 'back' is false - * @param sourceId (String) use this sourceId and ignore facing mode if specified. - * @return VideoCapturer can invoke with startCapture/stopCapture - * null if not matched camera with specified facing mode. + * + * @param enumerator a CameraEnumerator provided by webrtc it can be Camera1Enumerator or + * Camera2Enumerator + * @param isFacing 'user' mapped with 'front' is true (default) 'environment' mapped with 'back' + * is false + * @param sourceId (String) use this sourceId and ignore facing mode if specified. + * @return VideoCapturer can invoke with startCapture/stopCapture null + * if not matched camera with specified facing mode. */ private VideoCapturer createVideoCapturer( - CameraEnumerator enumerator, - boolean isFacing, - String sourceId) { + CameraEnumerator enumerator, boolean isFacing, String sourceId) { VideoCapturer videoCapturer = null; // if sourceId given, use specified sourceId first @@ -260,25 +281,18 @@ private VideoCapturer createVideoCapturer( /** * Retrieves "facingMode" constraint value. * - * @param mediaConstraints a ConstraintsMap which represents "GUM" - * constraints argument. - * @return String value of "facingMode" constraints in "GUM" or - * null if not specified. + * @param mediaConstraints a ConstraintsMap which represents "GUM" constraints argument. + * @return String value of "facingMode" constraints in "GUM" or null if not specified. */ private String getFacingMode(ConstraintsMap mediaConstraints) { - return - mediaConstraints == null - ? null - : mediaConstraints.getString("facingMode"); + return mediaConstraints == null ? null : mediaConstraints.getString("facingMode"); } /** * Retrieves "sourceId" constraint value. * - * @param mediaConstraints a ConstraintsMap which represents "GUM" - * constraints argument - * @return String value of "sourceId" optional "GUM" constraint or - * null if not specified. + * @param mediaConstraints a ConstraintsMap which represents "GUM" constraints argument + * @return String value of "sourceId" optional "GUM" constraint or null if not specified. */ private String getSourceIdConstraint(ConstraintsMap mediaConstraints) { if (mediaConstraints != null @@ -290,9 +304,7 @@ private String getSourceIdConstraint(ConstraintsMap mediaConstraints) { if (optional.getType(i) == ObjectType.Map) { ConstraintsMap option = optional.getMap(i); - if (option.hasKey("sourceId") - && option.getType("sourceId") - == ObjectType.String) { + if (option.hasKey("sourceId") && option.getType("sourceId") == ObjectType.String) { return option.getString("sourceId"); } } @@ -308,29 +320,25 @@ private AudioTrack getUserAudio(ConstraintsMap constraints) { audioConstraints = new MediaConstraints(); addDefaultAudioConstraints(audioConstraints); } else { - audioConstraints - = plugin.parseMediaConstraints( - constraints.getMap("audio")); + audioConstraints = MediaConstraintsUtils.parseMediaConstraints(constraints.getMap("audio")); } Log.i(TAG, "getUserMedia(audio): " + audioConstraints); - String trackId = plugin.getNextTrackUUID(); - PeerConnectionFactory pcFactory = plugin.mFactory; + String trackId = stateProvider.getNextTrackUUID(); + PeerConnectionFactory pcFactory = stateProvider.getPeerConnectionFactory(); AudioSource audioSource = pcFactory.createAudioSource(audioConstraints); return pcFactory.createAudioTrack(trackId, audioSource); } /** - * Implements {@code getUserMedia} without knowledge whether the necessary - * permissions have already been granted. If the necessary permissions have - * not been granted yet, they will be requested. + * Implements {@code getUserMedia} without knowledge whether the necessary permissions have + * already been granted. If the necessary permissions have not been granted yet, they will be + * requested. */ void getUserMedia( - final ConstraintsMap constraints, - final Result result, - final MediaStream mediaStream) { + final ConstraintsMap constraints, final Result result, final MediaStream mediaStream) { // TODO: change getUserMedia constraints format to support new syntax // constraint format seems changed, and there is no mandatory any more. @@ -338,16 +346,14 @@ void getUserMedia( // should change `parseConstraints()` according // see: https://www.w3.org/TR/mediacapture-streams/#idl-def-MediaTrackConstraints - ConstraintsMap videoConstraintsMap = null; - ConstraintsMap videoConstraintsMandatory = null; + ConstraintsMap videoConstraintsMap = null; + ConstraintsMap videoConstraintsMandatory = null; if (constraints.getType("video") == ObjectType.Map) { videoConstraintsMap = constraints.getMap("video"); if (videoConstraintsMap.hasKey("mandatory") - && videoConstraintsMap.getType("mandatory") - == ObjectType.Map) { - videoConstraintsMandatory - = videoConstraintsMap.getMap("mandatory"); + && videoConstraintsMap.getType("mandatory") == ObjectType.Map) { + videoConstraintsMandatory = videoConstraintsMap.getMap("mandatory"); } } @@ -355,31 +361,31 @@ void getUserMedia( if (constraints.hasKey("audio")) { switch (constraints.getType("audio")) { - case Boolean: - if (constraints.getBoolean("audio")) { + case Boolean: + if (constraints.getBoolean("audio")) { + requestPermissions.add(PERMISSION_AUDIO); + } + break; + case Map: requestPermissions.add(PERMISSION_AUDIO); - } - break; - case Map: - requestPermissions.add(PERMISSION_AUDIO); - break; - default: - break; + break; + default: + break; } } if (constraints.hasKey("video")) { switch (constraints.getType("video")) { - case Boolean: - if (constraints.getBoolean("video")) { + case Boolean: + if (constraints.getBoolean("video")) { + requestPermissions.add(PERMISSION_VIDEO); + } + break; + case Map: requestPermissions.add(PERMISSION_VIDEO); - } - break; - case Map: - requestPermissions.add(PERMISSION_VIDEO); - break; - default: - break; + break; + default: + break; } } @@ -390,162 +396,160 @@ void getUserMedia( // requestedMediaTypes is the empty set, the method invocation fails // with a TypeError. if (requestPermissions.isEmpty()) { - result.error( - "TypeError", - "constraints requests no media types", null); + result.error("TypeError", "constraints requests no media types", null); + return; + } + + /// Only systems pre-M, no additional permission request is needed. + if (VERSION.SDK_INT < VERSION_CODES.M) { + getUserMedia(constraints, result, mediaStream, requestPermissions); return; } requestPermissions( - requestPermissions, - /* successCallback */ new Callback() { - @Override - public void invoke(Object... args) { - List grantedPermissions = (List) args[0]; - - getUserMedia( - constraints, - result, - mediaStream, - grantedPermissions); - } - }, - /* errorCallback */ new Callback() { - @Override - public void invoke(Object... args) { - // According to step 10 Permission Failure of the - // getUserMedia() algorithm, if the user has denied - // permission, fail "with a new DOMException object whose - // name attribute has the value NotAllowedError." - result.error("DOMException", "NotAllowedError", null); - } - } - ); + requestPermissions, + /* successCallback */ new Callback() { + @Override + public void invoke(Object... args) { + List grantedPermissions = (List) args[0]; + + getUserMedia(constraints, result, mediaStream, grantedPermissions); + } + }, + /* errorCallback */ new Callback() { + @Override + public void invoke(Object... args) { + // According to step 10 Permission Failure of the + // getUserMedia() algorithm, if the user has denied + // permission, fail "with a new DOMException object whose + // name attribute has the value NotAllowedError." + result.error("DOMException", "NotAllowedError", null); + } + }); } void getDisplayMedia( - final ConstraintsMap constraints, - final Result result, - final MediaStream mediaStream) { - ConstraintsMap videoConstraintsMap = null; - ConstraintsMap videoConstraintsMandatory = null; + final ConstraintsMap constraints, final Result result, final MediaStream mediaStream) { + ConstraintsMap videoConstraintsMap = null; + ConstraintsMap videoConstraintsMandatory = null; if (constraints.getType("video") == ObjectType.Map) { videoConstraintsMap = constraints.getMap("video"); if (videoConstraintsMap.hasKey("mandatory") - && videoConstraintsMap.getType("mandatory") - == ObjectType.Map) { - videoConstraintsMandatory - = videoConstraintsMap.getMap("mandatory"); + && videoConstraintsMap.getType("mandatory") == ObjectType.Map) { + videoConstraintsMandatory = videoConstraintsMap.getMap("mandatory"); } } - final ConstraintsMap videoConstraintsMandatory2 = videoConstraintsMandatory; - screenRequestPremissions(new ResultReceiver(new Handler(Looper.getMainLooper())) { - @Override - protected void onReceiveResult( - int requestCode, - Bundle resultData) { - - /* Create ScreenCapture */ - int resultCode = resultData.getInt(GRANT_RESULTS); - Intent mediaProjectionData = resultData.getParcelable(PROJECTION_DATA); - - if (resultCode != Activity.RESULT_OK) { - result.error(null, "User didn't give permission to capture the screen.", null); - return; - } + final ConstraintsMap videoConstraintsMandatory2 = videoConstraintsMandatory; - MediaStreamTrack[] tracks = new MediaStreamTrack[1]; - VideoCapturer videoCapturer = null; - videoCapturer = new ScreenCapturerAndroid(mediaProjectionData, new MediaProjection.Callback() { + screenRequestPremissions( + new ResultReceiver(new Handler(Looper.getMainLooper())) { @Override - public void onStop() { - Log.e(TAG, "User revoked permission to capture the screen."); - result.error(null, "User revoked permission to capture the screen.", null); + protected void onReceiveResult(int requestCode, Bundle resultData) { + + /* Create ScreenCapture */ + int resultCode = resultData.getInt(GRANT_RESULTS); + Intent mediaProjectionData = resultData.getParcelable(PROJECTION_DATA); + + if (resultCode != Activity.RESULT_OK) { + result.error(null, "User didn't give permission to capture the screen.", null); + return; + } + + MediaStreamTrack[] tracks = new MediaStreamTrack[1]; + VideoCapturer videoCapturer = null; + videoCapturer = + new ScreenCapturerAndroid( + mediaProjectionData, + new MediaProjection.Callback() { + @Override + public void onStop() { + Log.e(TAG, "User revoked permission to capture the screen."); + result.error(null, "User revoked permission to capture the screen.", null); + } + }); + if (videoCapturer == null) { + result.error( + /* type */ "GetDisplayMediaFailed", "Failed to create new VideoCapturer!", null); + return; + } + + PeerConnectionFactory pcFactory = stateProvider.getPeerConnectionFactory(); + VideoSource videoSource = pcFactory.createVideoSource(true); + + String threadName = Thread.currentThread().getName(); + SurfaceTextureHelper surfaceTextureHelper = + SurfaceTextureHelper.create(threadName, EglUtils.getRootEglBaseContext()); + videoCapturer.initialize( + surfaceTextureHelper, applicationContext, videoSource.getCapturerObserver()); + + WindowManager wm = + (WindowManager) applicationContext.getSystemService(Context.WINDOW_SERVICE); + + int width = wm.getDefaultDisplay().getWidth(); + int height = wm.getDefaultDisplay().getHeight(); + int fps = DEFAULT_FPS; + + videoCapturer.startCapture(width, height, fps); + Log.d(TAG, "ScreenCapturerAndroid.startCapture: " + width + "x" + height + "@" + fps); + + String trackId = stateProvider.getNextTrackUUID(); + mVideoCapturers.put(trackId, videoCapturer); + + tracks[0] = pcFactory.createVideoTrack(trackId, videoSource); + + ConstraintsArray audioTracks = new ConstraintsArray(); + ConstraintsArray videoTracks = new ConstraintsArray(); + ConstraintsMap successResult = new ConstraintsMap(); + + for (MediaStreamTrack track : tracks) { + if (track == null) { + continue; + } + + String id = track.id(); + + if (track instanceof AudioTrack) { + mediaStream.addTrack((AudioTrack) track); + } else { + mediaStream.addTrack((VideoTrack) track); + } + stateProvider.getLocalTracks().put(id, track); + + ConstraintsMap track_ = new ConstraintsMap(); + String kind = track.kind(); + + track_.putBoolean("enabled", track.enabled()); + track_.putString("id", id); + track_.putString("kind", kind); + track_.putString("label", kind); + track_.putString("readyState", track.state().toString()); + track_.putBoolean("remote", false); + + if (track instanceof AudioTrack) { + audioTracks.pushMap(track_); + } else { + videoTracks.pushMap(track_); + } + } + + String streamId = mediaStream.getId(); + + Log.d(TAG, "MediaStream id: " + streamId); + stateProvider.getLocalStreams().put(streamId, mediaStream); + successResult.putString("streamId", streamId); + successResult.putArray("audioTracks", audioTracks.toArrayList()); + successResult.putArray("videoTracks", videoTracks.toArrayList()); + result.success(successResult.toMap()); } }); - if (videoCapturer == null) { - result.error( - /* type */ "GetDisplayMediaFailed", - "Failed to create new VideoCapturer!", null); - return; - } - - PeerConnectionFactory pcFactory = plugin.mFactory; - VideoSource videoSource = pcFactory.createVideoSource(true); - - Context context = plugin.getContext(); - String threadName = Thread.currentThread().getName(); - SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create(threadName, EglUtils.getRootEglBaseContext()); - videoCapturer.initialize(surfaceTextureHelper, context, videoSource.getCapturerObserver()); - - WindowManager wm = (WindowManager) applicationContext - .getSystemService(Context.WINDOW_SERVICE); - - int width = wm.getDefaultDisplay().getWidth(); - int height = wm.getDefaultDisplay().getHeight(); - int fps = DEFAULT_FPS; - - videoCapturer.startCapture(width, height, fps); - Log.d(TAG, "ScreenCapturerAndroid.startCapture: " + width + "x" + height + "@" + fps); - - String trackId = plugin.getNextTrackUUID(); - mVideoCapturers.put(trackId, videoCapturer); - - tracks[0] = pcFactory.createVideoTrack(trackId, videoSource); - - ConstraintsArray audioTracks = new ConstraintsArray(); - ConstraintsArray videoTracks = new ConstraintsArray(); - ConstraintsMap successResult = new ConstraintsMap(); - - for (MediaStreamTrack track : tracks) { - if (track == null) { - continue; - } - - String id = track.id(); - - if (track instanceof AudioTrack) { - mediaStream.addTrack((AudioTrack) track); - } else { - mediaStream.addTrack((VideoTrack) track); - } - plugin.localTracks.put(id, track); - - ConstraintsMap track_ = new ConstraintsMap(); - String kind = track.kind(); - - track_.putBoolean("enabled", track.enabled()); - track_.putString("id", id); - track_.putString("kind", kind); - track_.putString("label", kind); - track_.putString("readyState", track.state().toString()); - track_.putBoolean("remote", false); - - if (track instanceof AudioTrack) { - audioTracks.pushMap(track_); - } else { - videoTracks.pushMap(track_); - } - } - - String streamId = mediaStream.getId(); - - Log.d(TAG, "MediaStream id: " + streamId); - plugin.localStreams.put(streamId, mediaStream); - successResult.putString("streamId", streamId); - successResult.putArray("audioTracks", audioTracks.toArrayList()); - successResult.putArray("videoTracks", videoTracks.toArrayList()); - result.success(successResult.toMap()); - } - }); } /** - * Implements {@code getUserMedia} with the knowledge that the necessary - * permissions have already been granted. If the necessary permissions have - * not been granted yet, they will NOT be requested. + * Implements {@code getUserMedia} with the knowledge that the necessary permissions have already + * been granted. If the necessary permissions have not been granted yet, they will NOT be + * requested. */ private void getUserMedia( ConstraintsMap constraints, @@ -569,9 +573,7 @@ private void getUserMedia( // specified by // https://www.w3.org/TR/mediacapture-streams/#dom-mediadevices-getusermedia // with respect to distinguishing the various causes of failure. - result.error( - /* type */ "GetUserMediaFailed", - "Failed to create new track", null); + result.error(/* type */ "GetUserMediaFailed", "Failed to create new track", null); return; } @@ -591,7 +593,7 @@ private void getUserMedia( } else { mediaStream.addTrack((VideoTrack) track); } - plugin.localTracks.put(id, track); + stateProvider.getLocalTracks().put(id, track); ConstraintsMap track_ = new ConstraintsMap(); String kind = track.kind(); @@ -613,7 +615,7 @@ private void getUserMedia( String streamId = mediaStream.getId(); Log.d(TAG, "MediaStream id: " + streamId); - plugin.localStreams.put(streamId, mediaStream); + stateProvider.getLocalStreams().put(streamId, mediaStream); successResult.putString("streamId", streamId); successResult.putArray("audioTracks", audioTracks.toArrayList()); @@ -621,73 +623,70 @@ private void getUserMedia( result.success(successResult.toMap()); } - private VideoTrack getUserVideo(ConstraintsMap constraints) { ConstraintsMap videoConstraintsMap = null; ConstraintsMap videoConstraintsMandatory = null; if (constraints.getType("video") == ObjectType.Map) { videoConstraintsMap = constraints.getMap("video"); if (videoConstraintsMap.hasKey("mandatory") - && videoConstraintsMap.getType("mandatory") - == ObjectType.Map) { - videoConstraintsMandatory - = videoConstraintsMap.getMap("mandatory"); + && videoConstraintsMap.getType("mandatory") == ObjectType.Map) { + videoConstraintsMandatory = videoConstraintsMap.getMap("mandatory"); } } Log.i(TAG, "getUserMedia(video): " + videoConstraintsMap); - // NOTE: to support Camera2, the device should: - // 1. Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP - // 2. all camera support level should greater than LEGACY - // see: https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics.html#INFO_SUPPORTED_HARDWARE_LEVEL - // TODO Enable camera2 enumerator - Context context = plugin.getContext(); - CameraEnumerator cameraEnumerator; - - if (Camera2Enumerator.isSupported(context)) { - Log.d(TAG, "Creating video capturer using Camera2 API."); - cameraEnumerator = new Camera2Enumerator(context); - } else { - Log.d(TAG, "Creating video capturer using Camera1 API."); - cameraEnumerator = new Camera1Enumerator(false); - } + // NOTE: to support Camera2, the device should: + // 1. Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP + // 2. all camera support level should greater than LEGACY + // see: + // https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics.html#INFO_SUPPORTED_HARDWARE_LEVEL + // TODO Enable camera2 enumerator + CameraEnumerator cameraEnumerator; + + if (Camera2Enumerator.isSupported(applicationContext)) { + Log.d(TAG, "Creating video capturer using Camera2 API."); + cameraEnumerator = new Camera2Enumerator(applicationContext); + } else { + Log.d(TAG, "Creating video capturer using Camera1 API."); + cameraEnumerator = new Camera1Enumerator(false); + } - String facingMode = getFacingMode(videoConstraintsMap); - boolean isFacing - = facingMode == null || !facingMode.equals("environment"); + String facingMode = getFacingMode(videoConstraintsMap); + boolean isFacing = facingMode == null || !facingMode.equals("environment"); String sourceId = getSourceIdConstraint(videoConstraintsMap); - VideoCapturer videoCapturer - = createVideoCapturer(cameraEnumerator, isFacing, sourceId); + VideoCapturer videoCapturer = createVideoCapturer(cameraEnumerator, isFacing, sourceId); if (videoCapturer == null) { return null; } - PeerConnectionFactory pcFactory = plugin.mFactory; + PeerConnectionFactory pcFactory = stateProvider.getPeerConnectionFactory(); VideoSource videoSource = pcFactory.createVideoSource(false); String threadName = Thread.currentThread().getName(); - SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create(threadName, EglUtils.getRootEglBaseContext()); - videoCapturer.initialize(surfaceTextureHelper, context, videoSource.getCapturerObserver()); + SurfaceTextureHelper surfaceTextureHelper = + SurfaceTextureHelper.create(threadName, EglUtils.getRootEglBaseContext()); + videoCapturer.initialize( + surfaceTextureHelper, applicationContext, videoSource.getCapturerObserver()); // Fall back to defaults if keys are missing. - int width - = videoConstraintsMandatory != null && videoConstraintsMandatory.hasKey("minWidth") - ? videoConstraintsMandatory.getInt("minWidth") - : DEFAULT_WIDTH; - int height - = videoConstraintsMandatory != null && videoConstraintsMandatory.hasKey("minHeight") - ? videoConstraintsMandatory.getInt("minHeight") - : DEFAULT_HEIGHT; - int fps - = videoConstraintsMandatory != null && videoConstraintsMandatory.hasKey("minFrameRate") - ? videoConstraintsMandatory.getInt("minFrameRate") - : DEFAULT_FPS; + int width = + videoConstraintsMandatory != null && videoConstraintsMandatory.hasKey("minWidth") + ? videoConstraintsMandatory.getInt("minWidth") + : DEFAULT_WIDTH; + int height = + videoConstraintsMandatory != null && videoConstraintsMandatory.hasKey("minHeight") + ? videoConstraintsMandatory.getInt("minHeight") + : DEFAULT_HEIGHT; + int fps = + videoConstraintsMandatory != null && videoConstraintsMandatory.hasKey("minFrameRate") + ? videoConstraintsMandatory.getInt("minFrameRate") + : DEFAULT_FPS; videoCapturer.startCapture(width, height, fps); - String trackId = plugin.getNextTrackUUID(); + String trackId = stateProvider.getNextTrackUUID(); mVideoCapturers.put(trackId, videoCapturer); Log.d(TAG, "changeCaptureFormat: " + width + "x" + height + "@" + fps); @@ -710,45 +709,45 @@ void removeVideoCapturer(String id) { } } + @RequiresApi(api = VERSION_CODES.M) private void requestPermissions( final ArrayList permissions, final Callback successCallback, final Callback errorCallback) { - PermissionUtils.Callback callback = new PermissionUtils.Callback() { - @Override - public void invoke(String[] permissions_, int[] grantResults) { - List grantedPermissions = new ArrayList<>(); - List deniedPermissions = new ArrayList<>(); - - for (int i = 0; i < permissions_.length; ++i) { - String permission = permissions_[i]; - int grantResult = grantResults[i]; - - if (grantResult == PackageManager.PERMISSION_GRANTED) { - grantedPermissions.add(permission); - } else { - deniedPermissions.add(permission); + PermissionUtils.Callback callback = + (permissions_, grantResults) -> { + List grantedPermissions = new ArrayList<>(); + List deniedPermissions = new ArrayList<>(); + + for (int i = 0; i < permissions_.length; ++i) { + String permission = permissions_[i]; + int grantResult = grantResults[i]; + + if (grantResult == PackageManager.PERMISSION_GRANTED) { + grantedPermissions.add(permission); + } else { + deniedPermissions.add(permission); + } } - } - // Success means that all requested permissions were granted. - for (String p : permissions) { - if (!grantedPermissions.contains(p)) { - // According to step 6 of the getUserMedia() algorithm - // "if the result is denied, jump to the step Permission - // Failure." - errorCallback.invoke(deniedPermissions); - return; + // Success means that all requested permissions were granted. + for (String p : permissions) { + if (!grantedPermissions.contains(p)) { + // According to step 6 of the getUserMedia() algorithm + // "if the result is denied, jump to the step Permission + // Failure." + errorCallback.invoke(deniedPermissions); + return; + } } - } - successCallback.invoke(grantedPermissions); - } - }; + successCallback.invoke(grantedPermissions); + }; - PermissionUtils.requestPermissions( - plugin, - permissions.toArray(new String[permissions.size()]), - callback); + final Activity activity = stateProvider.getActivity(); + if (activity != null) { + PermissionUtils.requestPermissions( + activity, permissions.toArray(new String[permissions.size()]), callback); + } } void switchCamera(String id, Result result) { @@ -758,33 +757,39 @@ void switchCamera(String id, Result result) { return; } - CameraVideoCapturer cameraVideoCapturer - = (CameraVideoCapturer) videoCapturer; - cameraVideoCapturer.switchCamera(new CameraVideoCapturer.CameraSwitchHandler() { - @Override - public void onCameraSwitchDone(boolean b) { - result.success(b); - } - @Override - public void onCameraSwitchError(String s) { - result.error("Switching camera failed", s, null); - } - }); + CameraVideoCapturer cameraVideoCapturer = (CameraVideoCapturer) videoCapturer; + cameraVideoCapturer.switchCamera( + new CameraVideoCapturer.CameraSwitchHandler() { + @Override + public void onCameraSwitchDone(boolean b) { + result.success(b); + } + + @Override + public void onCameraSwitchError(String s) { + result.error("Switching camera failed", s, null); + } + }); } - /** Creates and starts recording of local stream to file - * @param path to the file for record - * @param videoTrack to record or null if only audio needed - * @param audioChannel channel for recording or null - * @throws Exception lot of different exceptions, pass back to dart layer to print them at least - * **/ - void startRecordingToFile(String path, Integer id, @Nullable VideoTrack videoTrack, @Nullable AudioChannel audioChannel) throws Exception { + /** + * Creates and starts recording of local stream to file + * + * @param path to the file for record + * @param videoTrack to record or null if only audio needed + * @param audioChannel channel for recording or null + * @throws Exception lot of different exceptions, pass back to dart layer to print them at least + */ + void startRecordingToFile( + String path, Integer id, @Nullable VideoTrack videoTrack, @Nullable AudioChannel audioChannel) + throws Exception { AudioSamplesInterceptor interceptor = null; - if (audioChannel == AudioChannel.INPUT) + if (audioChannel == AudioChannel.INPUT) { interceptor = inputSamplesInterceptor; - else if (audioChannel == AudioChannel.OUTPUT) { - if (outputSamplesInterceptor == null) + } else if (audioChannel == AudioChannel.OUTPUT) { + if (outputSamplesInterceptor == null) { outputSamplesInterceptor = new OutputAudioSamplesInterceptor(audioDeviceModule); + } interceptor = outputSamplesInterceptor; } MediaRecorderImpl mediaRecorder = new MediaRecorderImpl(id, videoTrack, interceptor); @@ -803,7 +808,9 @@ void stopRecording(Integer id) { values.put(MediaStore.Video.Media.TITLE, file.getName()); values.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4"); values.put(MediaStore.Video.Media.DATA, file.getAbsolutePath()); - applicationContext.getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values); + applicationContext + .getContentResolver() + .insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values); } } } @@ -815,14 +822,19 @@ void hasTorch(String trackId, Result result) { return; } - if (videoCapturer instanceof Camera2Capturer) { + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && videoCapturer instanceof Camera2Capturer) { CameraManager manager; CameraDevice cameraDevice; try { - Object session = getPrivateProperty(Camera2Capturer.class.getSuperclass(), videoCapturer, "currentSession"); - manager = (CameraManager) getPrivateProperty(Camera2Capturer.class, videoCapturer, "cameraManager"); - cameraDevice = (CameraDevice) getPrivateProperty(session.getClass(), session, "cameraDevice"); + Object session = + getPrivateProperty( + Camera2Capturer.class.getSuperclass(), videoCapturer, "currentSession"); + manager = + (CameraManager) + getPrivateProperty(Camera2Capturer.class, videoCapturer, "cameraManager"); + cameraDevice = + (CameraDevice) getPrivateProperty(session.getClass(), session, "cameraDevice"); } catch (NoSuchFieldWithNameException e) { // Most likely the upstream Camera2Capturer class have changed Log.e(TAG, "[TORCH] Failed to get `" + e.fieldName + "` from `" + e.className + "`"); @@ -832,7 +844,8 @@ void hasTorch(String trackId, Result result) { boolean flashIsAvailable; try { - CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraDevice.getId()); + CameraCharacteristics characteristics = + manager.getCameraCharacteristics(cameraDevice.getId()); flashIsAvailable = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); } catch (CameraAccessException e) { // Should never happen since we are already accessing the camera @@ -847,7 +860,9 @@ void hasTorch(String trackId, Result result) { Camera camera; try { - Object session = getPrivateProperty(Camera1Capturer.class.getSuperclass(), videoCapturer, "currentSession"); + Object session = + getPrivateProperty( + Camera1Capturer.class.getSuperclass(), videoCapturer, "currentSession"); camera = (Camera) getPrivateProperty(session.getClass(), session, "camera"); } catch (NoSuchFieldWithNameException e) { // Most likely the upstream Camera1Capturer class have changed @@ -856,10 +871,11 @@ void hasTorch(String trackId, Result result) { return; } - Camera.Parameters params = camera.getParameters(); + Parameters params = camera.getParameters(); List supportedModes = params.getSupportedFlashModes(); - result.success((supportedModes == null) ? false : supportedModes.contains(Camera.Parameters.FLASH_MODE_TORCH)); + result.success( + (supportedModes == null) ? false : supportedModes.contains(Parameters.FLASH_MODE_TORCH)); return; } @@ -867,6 +883,7 @@ void hasTorch(String trackId, Result result) { result.error(null, "Video capturer not compatible", null); } + @RequiresApi(api = VERSION_CODES.LOLLIPOP) void setTorch(String trackId, boolean torch, Result result) { VideoCapturer videoCapturer = mVideoCapturers.get(trackId); if (videoCapturer == null) { @@ -883,14 +900,23 @@ void setTorch(String trackId, boolean torch, Result result) { Handler cameraThreadHandler; try { - Object session = getPrivateProperty(Camera2Capturer.class.getSuperclass(), videoCapturer, "currentSession"); - CameraManager manager = (CameraManager) getPrivateProperty(Camera2Capturer.class, videoCapturer, "cameraManager"); - captureSession = (CameraCaptureSession) getPrivateProperty(session.getClass(), session, "captureSession"); - cameraDevice = (CameraDevice) getPrivateProperty(session.getClass(), session, "cameraDevice"); - captureFormat = (CaptureFormat) getPrivateProperty(session.getClass(), session, "captureFormat"); + Object session = + getPrivateProperty( + Camera2Capturer.class.getSuperclass(), videoCapturer, "currentSession"); + CameraManager manager = + (CameraManager) + getPrivateProperty(Camera2Capturer.class, videoCapturer, "cameraManager"); + captureSession = + (CameraCaptureSession) + getPrivateProperty(session.getClass(), session, "captureSession"); + cameraDevice = + (CameraDevice) getPrivateProperty(session.getClass(), session, "cameraDevice"); + captureFormat = + (CaptureFormat) getPrivateProperty(session.getClass(), session, "captureFormat"); fpsUnitFactor = (int) getPrivateProperty(session.getClass(), session, "fpsUnitFactor"); surface = (Surface) getPrivateProperty(session.getClass(), session, "surface"); - cameraThreadHandler = (Handler) getPrivateProperty(session.getClass(), session, "cameraThreadHandler"); + cameraThreadHandler = + (Handler) getPrivateProperty(session.getClass(), session, "cameraThreadHandler"); } catch (NoSuchFieldWithNameException e) { // Most likely the upstream Camera2Capturer class have changed Log.e(TAG, "[TORCH] Failed to get `" + e.fieldName + "` from `" + e.className + "`"); @@ -899,16 +925,22 @@ void setTorch(String trackId, boolean torch, Result result) { } try { - final CaptureRequest.Builder captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD); - captureRequestBuilder.set(CaptureRequest.FLASH_MODE, torch ? CaptureRequest.FLASH_MODE_TORCH : CaptureRequest.FLASH_MODE_OFF); - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, - new Range(captureFormat.framerate.min / fpsUnitFactor, - captureFormat.framerate.max / fpsUnitFactor)); + final CaptureRequest.Builder captureRequestBuilder = + cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD); captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + CaptureRequest.FLASH_MODE, + torch ? CaptureRequest.FLASH_MODE_TORCH : CaptureRequest.FLASH_MODE_OFF); + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, + new Range<>( + captureFormat.framerate.min / fpsUnitFactor, + captureFormat.framerate.max / fpsUnitFactor)); + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); captureRequestBuilder.addTarget(surface); - captureSession.setRepeatingRequest(captureRequestBuilder.build(), null, cameraThreadHandler); + captureSession.setRepeatingRequest( + captureRequestBuilder.build(), null, cameraThreadHandler); } catch (CameraAccessException e) { // Should never happen since we are already accessing the camera throw new RuntimeException(e); @@ -921,7 +953,9 @@ void setTorch(String trackId, boolean torch, Result result) { if (videoCapturer instanceof Camera1Capturer) { Camera camera; try { - Object session = getPrivateProperty(Camera1Capturer.class.getSuperclass(), videoCapturer, "currentSession"); + Object session = + getPrivateProperty( + Camera1Capturer.class.getSuperclass(), videoCapturer, "currentSession"); camera = (Camera) getPrivateProperty(session.getClass(), session, "camera"); } catch (NoSuchFieldWithNameException e) { // Most likely the upstream Camera1Capturer class have changed @@ -931,7 +965,8 @@ void setTorch(String trackId, boolean torch, Result result) { } Camera.Parameters params = camera.getParameters(); - params.setFlashMode(torch ? Camera.Parameters.FLASH_MODE_TORCH : Camera.Parameters.FLASH_MODE_OFF); + params.setFlashMode( + torch ? Camera.Parameters.FLASH_MODE_TORCH : Camera.Parameters.FLASH_MODE_OFF); camera.setParameters(params); result.success(null); @@ -942,7 +977,8 @@ void setTorch(String trackId, boolean torch, Result result) { result.error(null, "Video capturer not compatible", null); } - private Object getPrivateProperty (Class klass, Object object, String fieldName) throws NoSuchFieldWithNameException { + private Object getPrivateProperty(Class klass, Object object, String fieldName) + throws NoSuchFieldWithNameException { try { Field field = klass.getDeclaredField(fieldName); field.setAccessible(true); @@ -956,6 +992,7 @@ private Object getPrivateProperty (Class klass, Object object, String fieldName) } private class NoSuchFieldWithNameException extends NoSuchFieldException { + String className; String fieldName; diff --git a/android/src/main/java/com/cloudwebrtc/webrtc/MethodCallHandlerImpl.java b/android/src/main/java/com/cloudwebrtc/webrtc/MethodCallHandlerImpl.java new file mode 100644 index 0000000000..a4e18a0344 --- /dev/null +++ b/android/src/main/java/com/cloudwebrtc/webrtc/MethodCallHandlerImpl.java @@ -0,0 +1,1404 @@ +package com.cloudwebrtc.webrtc; + +import static com.cloudwebrtc.webrtc.utils.MediaConstraintsUtils.parseMediaConstraints; + +import android.app.Activity; +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.hardware.Camera.CameraInfo; +import android.util.Log; +import android.util.LongSparseArray; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.cloudwebrtc.webrtc.record.AudioChannel; +import com.cloudwebrtc.webrtc.record.FrameCapturer; +import com.cloudwebrtc.webrtc.utils.AnyThreadResult; +import com.cloudwebrtc.webrtc.utils.ConstraintsArray; +import com.cloudwebrtc.webrtc.utils.ConstraintsMap; +import com.cloudwebrtc.webrtc.utils.EglUtils; +import com.cloudwebrtc.webrtc.utils.ObjectType; +import com.cloudwebrtc.webrtc.utils.RTCAudioManager; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.view.TextureRegistry; +import io.flutter.view.TextureRegistry.SurfaceTextureEntry; +import java.io.File; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.UUID; +import org.webrtc.AudioTrack; +import org.webrtc.DefaultVideoDecoderFactory; +import org.webrtc.DefaultVideoEncoderFactory; +import org.webrtc.EglBase; +import org.webrtc.IceCandidate; +import org.webrtc.Logging; +import org.webrtc.MediaConstraints; +import org.webrtc.MediaConstraints.KeyValuePair; +import org.webrtc.MediaStream; +import org.webrtc.MediaStreamTrack; +import org.webrtc.PeerConnection; +import org.webrtc.PeerConnection.BundlePolicy; +import org.webrtc.PeerConnection.CandidateNetworkPolicy; +import org.webrtc.PeerConnection.ContinualGatheringPolicy; +import org.webrtc.PeerConnection.IceServer; +import org.webrtc.PeerConnection.IceServer.Builder; +import org.webrtc.PeerConnection.IceTransportsType; +import org.webrtc.PeerConnection.KeyType; +import org.webrtc.PeerConnection.RTCConfiguration; +import org.webrtc.PeerConnection.RtcpMuxPolicy; +import org.webrtc.PeerConnection.SdpSemantics; +import org.webrtc.PeerConnection.TcpCandidatePolicy; +import org.webrtc.PeerConnectionFactory; +import org.webrtc.PeerConnectionFactory.InitializationOptions; +import org.webrtc.PeerConnectionFactory.Options; +import org.webrtc.SdpObserver; +import org.webrtc.SessionDescription; +import org.webrtc.SessionDescription.Type; +import org.webrtc.VideoTrack; +import org.webrtc.audio.AudioDeviceModule; +import org.webrtc.audio.JavaAudioDeviceModule; + +public class MethodCallHandlerImpl implements MethodCallHandler, StateProvider { + + + interface AudioManager { + + void onAudioManagerRequested(boolean requested); + + void setMicrophoneMute(boolean mute); + + void setSpeakerphoneOn(boolean on); + + + } + + static public final String TAG = "FlutterWebRTCPlugin"; + + private final Map mPeerConnectionObservers = new HashMap<>(); + private BinaryMessenger messenger; + private Context context; + private final TextureRegistry textures; + + private PeerConnectionFactory mFactory; + + private final Map localStreams = new HashMap<>(); + private final Map localTracks = new HashMap<>(); + + private LongSparseArray renders = new LongSparseArray<>(); + + /** + * The implementation of {@code getUserMedia} extracted into a separate file in order to reduce + * complexity and to (somewhat) separate concerns. + */ + private GetUserMediaImpl getUserMediaImpl; + + private final AudioManager audioManager; + + private AudioDeviceModule audioDeviceModule; + + private Activity activity; + + MethodCallHandlerImpl(Context context, BinaryMessenger messenger, TextureRegistry textureRegistry, + @NonNull AudioManager audioManager) { + this.context = context; + this.textures = textureRegistry; + this.messenger = messenger; + this.audioManager = audioManager; + } + + void dispose() { + mPeerConnectionObservers.clear(); + } + + private void ensureInitialized() { + if (mFactory != null) { + return; + } + + PeerConnectionFactory.initialize( + InitializationOptions.builder(context) + .setEnableInternalTracer(true) + .createInitializationOptions()); + + // Initialize EGL contexts required for HW acceleration. + EglBase.Context eglContext = EglUtils.getRootEglBaseContext(); + + getUserMediaImpl = new GetUserMediaImpl(this, context); + + audioDeviceModule = JavaAudioDeviceModule.builder(context) + .setUseHardwareAcousticEchoCanceler(true) + .setUseHardwareNoiseSuppressor(true) + .setSamplesReadyCallback(getUserMediaImpl.inputSamplesInterceptor) + .createAudioDeviceModule(); + + getUserMediaImpl.audioDeviceModule = (JavaAudioDeviceModule) audioDeviceModule; + + mFactory = PeerConnectionFactory.builder() + .setOptions(new Options()) + .setVideoEncoderFactory(new DefaultVideoEncoderFactory(eglContext, false, true)) + .setVideoDecoderFactory(new DefaultVideoDecoderFactory(eglContext)) + .setAudioDeviceModule(audioDeviceModule) + .createPeerConnectionFactory(); + } + + @Override + public void onMethodCall(MethodCall call, @NonNull Result notSafeResult) { + ensureInitialized(); + + final AnyThreadResult result = new AnyThreadResult(notSafeResult); + switch (call.method) { + case "createPeerConnection": { + Map constraints = call.argument("constraints"); + Map configuration = call.argument("configuration"); + String peerConnectionId = peerConnectionInit(new ConstraintsMap(configuration), + new ConstraintsMap((constraints))); + ConstraintsMap res = new ConstraintsMap(); + res.putString("peerConnectionId", peerConnectionId); + result.success(res.toMap()); + break; + } + case "getUserMedia": { + Map constraints = call.argument("constraints"); + ConstraintsMap constraintsMap = new ConstraintsMap(constraints); + getUserMedia(constraintsMap, result); + break; + } + case "createLocalMediaStream": + createLocalMediaStream(result); + break; + case "getSources": + getSources(result); + break; + case "createOffer": { + String peerConnectionId = call.argument("peerConnectionId"); + Map constraints = call.argument("constraints"); + peerConnectionCreateOffer(peerConnectionId, new ConstraintsMap(constraints), result); + break; + } + case "createAnswer": { + String peerConnectionId = call.argument("peerConnectionId"); + Map constraints = call.argument("constraints"); + peerConnectionCreateAnswer(peerConnectionId, new ConstraintsMap(constraints), result); + break; + } + case "mediaStreamGetTracks": { + String streamId = call.argument("streamId"); + MediaStream stream = getStreamForId(streamId, ""); + Map resultMap = new HashMap<>(); + List audioTracks = new ArrayList<>(); + List videoTracks = new ArrayList<>(); + for (AudioTrack track : stream.audioTracks) { + localTracks.put(track.id(), track); + Map trackMap = new HashMap<>(); + trackMap.put("enabled", track.enabled()); + trackMap.put("id", track.id()); + trackMap.put("kind", track.kind()); + trackMap.put("label", track.id()); + trackMap.put("readyState", "live"); + trackMap.put("remote", false); + audioTracks.add(trackMap); + } + for (VideoTrack track : stream.videoTracks) { + localTracks.put(track.id(), track); + Map trackMap = new HashMap<>(); + trackMap.put("enabled", track.enabled()); + trackMap.put("id", track.id()); + trackMap.put("kind", track.kind()); + trackMap.put("label", track.id()); + trackMap.put("readyState", "live"); + trackMap.put("remote", false); + videoTracks.add(trackMap); + } + resultMap.put("audioTracks", audioTracks); + resultMap.put("videoTracks", videoTracks); + result.success(resultMap); + break; + } + case "addStream": { + String streamId = call.argument("streamId"); + String peerConnectionId = call.argument("peerConnectionId"); + peerConnectionAddStream(streamId, peerConnectionId, result); + break; + } + case "removeStream": { + String streamId = call.argument("streamId"); + String peerConnectionId = call.argument("peerConnectionId"); + peerConnectionRemoveStream(streamId, peerConnectionId, result); + break; + } + case "setLocalDescription": { + String peerConnectionId = call.argument("peerConnectionId"); + Map description = call.argument("description"); + peerConnectionSetLocalDescription(new ConstraintsMap(description), peerConnectionId, + result); + break; + } + case "setRemoteDescription": { + String peerConnectionId = call.argument("peerConnectionId"); + Map description = call.argument("description"); + peerConnectionSetRemoteDescription(new ConstraintsMap(description), peerConnectionId, + result); + break; + } + case "addCandidate": { + String peerConnectionId = call.argument("peerConnectionId"); + Map candidate = call.argument("candidate"); + peerConnectionAddICECandidate(new ConstraintsMap(candidate), peerConnectionId, result); + break; + } + case "getStats": { + String peerConnectionId = call.argument("peerConnectionId"); + String trackId = call.argument("trackId"); + peerConnectionGetStats(trackId, peerConnectionId, result); + break; + } + case "createDataChannel": { + String peerConnectionId = call.argument("peerConnectionId"); + String label = call.argument("label"); + Map dataChannelDict = call.argument("dataChannelDict"); + createDataChannel(peerConnectionId, label, new ConstraintsMap(dataChannelDict), result); + break; + } + case "dataChannelSend": { + String peerConnectionId = call.argument("peerConnectionId"); + int dataChannelId = call.argument("dataChannelId"); + String type = call.argument("type"); + Boolean isBinary = type.equals("binary"); + ByteBuffer byteBuffer; + if (isBinary) { + byteBuffer = ByteBuffer.wrap(call.argument("data")); + } else { + try { + String data = call.argument("data"); + byteBuffer = ByteBuffer.wrap(data.getBytes("UTF-8")); + } catch (UnsupportedEncodingException e) { + Log.d(TAG, "Could not encode text string as UTF-8."); + result.error("dataChannelSendFailed", "Could not encode text string as UTF-8.", null); + return; + } + } + dataChannelSend(peerConnectionId, dataChannelId, byteBuffer, isBinary); + result.success(null); + break; + } + case "dataChannelClose": { + String peerConnectionId = call.argument("peerConnectionId"); + int dataChannelId = call.argument("dataChannelId"); + dataChannelClose(peerConnectionId, dataChannelId); + result.success(null); + break; + } + case "streamDispose": { + String streamId = call.argument("streamId"); + mediaStreamRelease(streamId); + result.success(null); + break; + } + case "mediaStreamTrackSetEnable": { + String trackId = call.argument("trackId"); + Boolean enabled = call.argument("enabled"); + MediaStreamTrack track = getTrackForId(trackId); + if (track != null) { + track.setEnabled(enabled); + } + result.success(null); + break; + } + case "mediaStreamAddTrack": { + String streamId = call.argument("streamId"); + String trackId = call.argument("trackId"); + mediaStreamAddTrack(streamId, trackId, result); + break; + } + case "mediaStreamRemoveTrack": { + String streamId = call.argument("streamId"); + String trackId = call.argument("trackId"); + mediaStreamRemoveTrack(streamId, trackId, result); + break; + } + case "trackDispose": { + String trackId = call.argument("trackId"); + localTracks.remove(trackId); + result.success(null); + break; + } + case "peerConnectionClose": { + String peerConnectionId = call.argument("peerConnectionId"); + peerConnectionClose(peerConnectionId); + result.success(null); + break; + } + case "peerConnectionDispose": { + String peerConnectionId = call.argument("peerConnectionId"); + peerConnectionDispose(peerConnectionId); + result.success(null); + break; + } + case "createVideoRenderer": { + SurfaceTextureEntry entry = textures.createSurfaceTexture(); + SurfaceTexture surfaceTexture = entry.surfaceTexture(); + FlutterRTCVideoRenderer render = new FlutterRTCVideoRenderer(surfaceTexture, entry); + renders.put(entry.id(), render); + + EventChannel eventChannel = + new EventChannel( + messenger, + "FlutterWebRTC/Texture" + entry.id()); + + eventChannel.setStreamHandler(render); + render.setEventChannel(eventChannel); + render.setId((int) entry.id()); + + ConstraintsMap params = new ConstraintsMap(); + params.putInt("textureId", (int) entry.id()); + result.success(params.toMap()); + break; + } + case "videoRendererDispose": { + int textureId = call.argument("textureId"); + FlutterRTCVideoRenderer render = renders.get(textureId); + if (render == null) { + result.error("FlutterRTCVideoRendererNotFound", "render [" + textureId + "] not found !", + null); + return; + } + render.Dispose(); + renders.delete(textureId); + result.success(null); + break; + } + case "videoRendererSetSrcObject": { + int textureId = call.argument("textureId"); + String streamId = call.argument("streamId"); + String peerConnectionId = call.argument("ownerTag"); + FlutterRTCVideoRenderer render = renders.get(textureId); + + if (render == null) { + result.error("FlutterRTCVideoRendererNotFound", "render [" + textureId + "] not found !", + null); + return; + } + + MediaStream stream = getStreamForId(streamId, peerConnectionId); + render.setStream(stream); + result.success(null); + break; + } + case "mediaStreamTrackHasTorch": { + String trackId = call.argument("trackId"); + getUserMediaImpl.hasTorch(trackId, result); + break; + } + case "mediaStreamTrackSetTorch": { + String trackId = call.argument("trackId"); + boolean torch = call.argument("torch"); + getUserMediaImpl.setTorch(trackId, torch, result); + break; + } + case "mediaStreamTrackSwitchCamera": { + String trackId = call.argument("trackId"); + getUserMediaImpl.switchCamera(trackId, result); + break; + } + case "setVolume": { + String trackId = call.argument("trackId"); + double volume = call.argument("volume"); + mediaStreamTrackSetVolume(trackId, volume); + result.success(null); + break; + } + case "setMicrophoneMute": + boolean mute = call.argument("mute"); + audioManager.setMicrophoneMute(mute); + result.success(null); + break; + case "enableSpeakerphone": + boolean enable = call.argument("enable"); + audioManager.setSpeakerphoneOn(enable); + result.success(null); + break; + case "getDisplayMedia": { + Map constraints = call.argument("constraints"); + ConstraintsMap constraintsMap = new ConstraintsMap(constraints); + getDisplayMedia(constraintsMap, result); + break; + } + case "startRecordToFile": + //This method can a lot of different exceptions + //so we should notify plugin user about them + try { + String path = call.argument("path"); + VideoTrack videoTrack = null; + String videoTrackId = call.argument("videoTrackId"); + if (videoTrackId != null) { + MediaStreamTrack track = getTrackForId(videoTrackId); + if (track instanceof VideoTrack) { + videoTrack = (VideoTrack) track; + } + } + AudioChannel audioChannel = null; + if (call.hasArgument("audioChannel")) { + audioChannel = AudioChannel.values()[(Integer) call.argument("audioChannel")]; + } + Integer recorderId = call.argument("recorderId"); + if (videoTrack != null || audioChannel != null) { + getUserMediaImpl.startRecordingToFile(path, recorderId, videoTrack, audioChannel); + result.success(null); + } else { + result.error("0", "No tracks", null); + } + } catch (Exception e) { + result.error("-1", e.getMessage(), e); + } + break; + case "stopRecordToFile": + Integer recorderId = call.argument("recorderId"); + getUserMediaImpl.stopRecording(recorderId); + result.success(null); + break; + case "captureFrame": + String path = call.argument("path"); + String videoTrackId = call.argument("trackId"); + if (videoTrackId != null) { + MediaStreamTrack track = getTrackForId(videoTrackId); + if (track instanceof VideoTrack) { + new FrameCapturer((VideoTrack) track, new File(path), result); + } else { + result.error(null, "It's not video track", null); + } + } else { + result.error(null, "Track is null", null); + } + break; + case "getLocalDescription": { + String peerConnectionId = call.argument("peerConnectionId"); + PeerConnection peerConnection = getPeerConnection(peerConnectionId); + if (peerConnection != null) { + SessionDescription sdp = peerConnection.getLocalDescription(); + ConstraintsMap params = new ConstraintsMap(); + params.putString("sdp", sdp.description); + params.putString("type", sdp.type.canonicalForm()); + result.success(params.toMap()); + } else { + Log.d(TAG, "getLocalDescription() peerConnection is null"); + result.error("getLocalDescriptionFailed", "getLocalDescription() peerConnection is null", + null); + } + break; + } + case "getRemoteDescription": { + String peerConnectionId = call.argument("peerConnectionId"); + PeerConnection peerConnection = getPeerConnection(peerConnectionId); + if (peerConnection != null) { + SessionDescription sdp = peerConnection.getRemoteDescription(); + ConstraintsMap params = new ConstraintsMap(); + params.putString("sdp", sdp.description); + params.putString("type", sdp.type.canonicalForm()); + result.success(params.toMap()); + } else { + Log.d(TAG, "getRemoteDescription() peerConnection is null"); + result + .error("getRemoteDescriptionFailed", "getRemoteDescription() peerConnection is null", + null); + } + break; + } + case "setConfiguration": { + String peerConnectionId = call.argument("peerConnectionId"); + Map configuration = call.argument("configuration"); + PeerConnection peerConnection = getPeerConnection(peerConnectionId); + if (peerConnection != null) { + peerConnectionSetConfiguration(new ConstraintsMap(configuration), peerConnection); + result.success(null); + } else { + Log.d(TAG, "setConfiguration() peerConnection is null"); + result.error("setConfigurationFailed", "setConfiguration() peerConnection is null", null); + } + break; + } + default: + result.notImplemented(); + break; + } + } + + private PeerConnection getPeerConnection(String id) { + PeerConnectionObserver pco = mPeerConnectionObservers.get(id); + return (pco == null) ? null : pco.getPeerConnection(); + } + + private List createIceServers(ConstraintsArray iceServersArray) { + final int size = (iceServersArray == null) ? 0 : iceServersArray.size(); + List iceServers = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + ConstraintsMap iceServerMap = iceServersArray.getMap(i); + boolean hasUsernameAndCredential = + iceServerMap.hasKey("username") && iceServerMap.hasKey("credential"); + if (iceServerMap.hasKey("url")) { + if (hasUsernameAndCredential) { + iceServers.add(IceServer.builder(iceServerMap.getString("url")) + .setUsername(iceServerMap.getString("username")) + .setPassword(iceServerMap.getString("credential")).createIceServer()); + } else { + iceServers.add( + IceServer.builder(iceServerMap.getString("url")).createIceServer()); + } + } else if (iceServerMap.hasKey("urls")) { + switch (iceServerMap.getType("urls")) { + case String: + if (hasUsernameAndCredential) { + iceServers.add(IceServer.builder(iceServerMap.getString("urls")) + .setUsername(iceServerMap.getString("username")) + .setPassword(iceServerMap.getString("credential")).createIceServer()); + } else { + iceServers.add(IceServer.builder(iceServerMap.getString("urls")) + .createIceServer()); + } + break; + case Array: + ConstraintsArray urls = iceServerMap.getArray("urls"); + List urlsList = new ArrayList<>(); + + for (int j = 0; j < urls.size(); j++) { + urlsList.add(urls.getString(j)); + } + + Builder builder = IceServer.builder(urlsList); + + if (hasUsernameAndCredential) { + builder + .setUsername(iceServerMap.getString("username")) + .setPassword(iceServerMap.getString("credential")); + } + + iceServers.add(builder.createIceServer()); + + break; + } + } + } + return iceServers; + } + + private RTCConfiguration parseRTCConfiguration(ConstraintsMap map) { + ConstraintsArray iceServersArray = null; + if (map != null) { + iceServersArray = map.getArray("iceServers"); + } + List iceServers = createIceServers(iceServersArray); + RTCConfiguration conf = new RTCConfiguration(iceServers); + if (map == null) { + return conf; + } + + // iceTransportPolicy (public api) + if (map.hasKey("iceTransportPolicy") + && map.getType("iceTransportPolicy") == ObjectType.String) { + final String v = map.getString("iceTransportPolicy"); + if (v != null) { + switch (v) { + case "all": // public + conf.iceTransportsType = IceTransportsType.ALL; + break; + case "relay": // public + conf.iceTransportsType = IceTransportsType.RELAY; + break; + case "nohost": + conf.iceTransportsType = IceTransportsType.NOHOST; + break; + case "none": + conf.iceTransportsType = IceTransportsType.NONE; + break; + } + } + } + + // bundlePolicy (public api) + if (map.hasKey("bundlePolicy") + && map.getType("bundlePolicy") == ObjectType.String) { + final String v = map.getString("bundlePolicy"); + if (v != null) { + switch (v) { + case "balanced": // public + conf.bundlePolicy = BundlePolicy.BALANCED; + break; + case "max-compat": // public + conf.bundlePolicy = BundlePolicy.MAXCOMPAT; + break; + case "max-bundle": // public + conf.bundlePolicy = BundlePolicy.MAXBUNDLE; + break; + } + } + } + + // rtcpMuxPolicy (public api) + if (map.hasKey("rtcpMuxPolicy") + && map.getType("rtcpMuxPolicy") == ObjectType.String) { + final String v = map.getString("rtcpMuxPolicy"); + if (v != null) { + switch (v) { + case "negotiate": // public + conf.rtcpMuxPolicy = RtcpMuxPolicy.NEGOTIATE; + break; + case "require": // public + conf.rtcpMuxPolicy = RtcpMuxPolicy.REQUIRE; + break; + } + } + } + + // FIXME: peerIdentity of type DOMString (public api) + // FIXME: certificates of type sequence (public api) + + // iceCandidatePoolSize of type unsigned short, defaulting to 0 + if (map.hasKey("iceCandidatePoolSize") + && map.getType("iceCandidatePoolSize") == ObjectType.Number) { + final int v = map.getInt("iceCandidatePoolSize"); + if (v > 0) { + conf.iceCandidatePoolSize = v; + } + } + + // sdpSemantics + if (map.hasKey("sdpSemantics") + && map.getType("sdpSemantics") == ObjectType.String) { + final String v = map.getString("sdpSemantics"); + if (v != null) { + switch (v) { + case "plan-b": + conf.sdpSemantics = SdpSemantics.PLAN_B; + break; + case "unified-plan": + conf.sdpSemantics = SdpSemantics.UNIFIED_PLAN; + break; + } + } + } + + // === below is private api in webrtc === + + // tcpCandidatePolicy (private api) + if (map.hasKey("tcpCandidatePolicy") + && map.getType("tcpCandidatePolicy") == ObjectType.String) { + final String v = map.getString("tcpCandidatePolicy"); + if (v != null) { + switch (v) { + case "enabled": + conf.tcpCandidatePolicy = TcpCandidatePolicy.ENABLED; + break; + case "disabled": + conf.tcpCandidatePolicy = TcpCandidatePolicy.DISABLED; + break; + } + } + } + + // candidateNetworkPolicy (private api) + if (map.hasKey("candidateNetworkPolicy") + && map.getType("candidateNetworkPolicy") == ObjectType.String) { + final String v = map.getString("candidateNetworkPolicy"); + if (v != null) { + switch (v) { + case "all": + conf.candidateNetworkPolicy = CandidateNetworkPolicy.ALL; + break; + case "low_cost": + conf.candidateNetworkPolicy = CandidateNetworkPolicy.LOW_COST; + break; + } + } + } + + // KeyType (private api) + if (map.hasKey("keyType") + && map.getType("keyType") == ObjectType.String) { + final String v = map.getString("keyType"); + if (v != null) { + switch (v) { + case "RSA": + conf.keyType = KeyType.RSA; + break; + case "ECDSA": + conf.keyType = KeyType.ECDSA; + break; + } + } + } + + // continualGatheringPolicy (private api) + if (map.hasKey("continualGatheringPolicy") + && map.getType("continualGatheringPolicy") == ObjectType.String) { + final String v = map.getString("continualGatheringPolicy"); + if (v != null) { + switch (v) { + case "gather_once": + conf.continualGatheringPolicy = ContinualGatheringPolicy.GATHER_ONCE; + break; + case "gather_continually": + conf.continualGatheringPolicy = ContinualGatheringPolicy.GATHER_CONTINUALLY; + break; + } + } + } + + // audioJitterBufferMaxPackets (private api) + if (map.hasKey("audioJitterBufferMaxPackets") + && map.getType("audioJitterBufferMaxPackets") == ObjectType.Number) { + final int v = map.getInt("audioJitterBufferMaxPackets"); + if (v > 0) { + conf.audioJitterBufferMaxPackets = v; + } + } + + // iceConnectionReceivingTimeout (private api) + if (map.hasKey("iceConnectionReceivingTimeout") + && map.getType("iceConnectionReceivingTimeout") == ObjectType.Number) { + final int v = map.getInt("iceConnectionReceivingTimeout"); + conf.iceConnectionReceivingTimeout = v; + } + + // iceBackupCandidatePairPingInterval (private api) + if (map.hasKey("iceBackupCandidatePairPingInterval") + && map.getType("iceBackupCandidatePairPingInterval") == ObjectType.Number) { + final int v = map.getInt("iceBackupCandidatePairPingInterval"); + conf.iceBackupCandidatePairPingInterval = v; + } + + // audioJitterBufferFastAccelerate (private api) + if (map.hasKey("audioJitterBufferFastAccelerate") + && map.getType("audioJitterBufferFastAccelerate") == ObjectType.Boolean) { + final boolean v = map.getBoolean("audioJitterBufferFastAccelerate"); + conf.audioJitterBufferFastAccelerate = v; + } + + // pruneTurnPorts (private api) + if (map.hasKey("pruneTurnPorts") + && map.getType("pruneTurnPorts") == ObjectType.Boolean) { + final boolean v = map.getBoolean("pruneTurnPorts"); + conf.pruneTurnPorts = v; + } + + // presumeWritableWhenFullyRelayed (private api) + if (map.hasKey("presumeWritableWhenFullyRelayed") + && map.getType("presumeWritableWhenFullyRelayed") == ObjectType.Boolean) { + final boolean v = map.getBoolean("presumeWritableWhenFullyRelayed"); + conf.presumeWritableWhenFullyRelayed = v; + } + + return conf; + } + + public String peerConnectionInit(ConstraintsMap configuration, ConstraintsMap constraints) { + String peerConnectionId = getNextStreamUUID(); + PeerConnectionObserver observer = new PeerConnectionObserver(this, messenger, peerConnectionId); + PeerConnection peerConnection + = mFactory.createPeerConnection( + parseRTCConfiguration(configuration), + parseMediaConstraints(constraints), + observer); + observer.setPeerConnection(peerConnection); + if (mPeerConnectionObservers.size() == 0) { + audioManager.onAudioManagerRequested(true); + } + mPeerConnectionObservers.put(peerConnectionId, observer); + return peerConnectionId; + } + + @Override + public Map getLocalStreams() { + return localStreams; + } + + @Override + public Map getLocalTracks() { + return localTracks; + } + + @Override + public String getNextStreamUUID() { + String uuid; + + do { + uuid = UUID.randomUUID().toString(); + } while (getStreamForId(uuid, "") != null); + + return uuid; + } + + @Override + public String getNextTrackUUID() { + String uuid; + + do { + uuid = UUID.randomUUID().toString(); + } while (getTrackForId(uuid) != null); + + return uuid; + } + + @Override + public PeerConnectionFactory getPeerConnectionFactory() { + return mFactory; + } + + @Nullable + @Override + public Activity getActivity() { + return activity; + } + + MediaStream getStreamForId(String id, String peerConnectionId) { + MediaStream stream = localStreams.get(id); + + if (stream == null) { + if (peerConnectionId.length() > 0) { + PeerConnectionObserver pco = mPeerConnectionObservers.get(peerConnectionId); + stream = pco.remoteStreams.get(id); + } else { + for (Entry entry : mPeerConnectionObservers + .entrySet()) { + PeerConnectionObserver pco = entry.getValue(); + stream = pco.remoteStreams.get(id); + if (stream != null) { + break; + } + } + } + } + + return stream; + } + + private MediaStreamTrack getTrackForId(String trackId) { + MediaStreamTrack track = localTracks.get(trackId); + + if (track == null) { + for (Entry entry : mPeerConnectionObservers.entrySet()) { + PeerConnectionObserver pco = entry.getValue(); + track = pco.remoteTracks.get(trackId); + if (track != null) { + break; + } + } + } + + return track; + } + + + public void getUserMedia(ConstraintsMap constraints, Result result) { + String streamId = getNextStreamUUID(); + MediaStream mediaStream = mFactory.createLocalMediaStream(streamId); + + if (mediaStream == null) { + // XXX The following does not follow the getUserMedia() algorithm + // specified by + // https://www.w3.org/TR/mediacapture-streams/#dom-mediadevices-getusermedia + // with respect to distinguishing the various causes of failure. + result.error( + /* type */ "getUserMediaFailed", + "Failed to create new media stream", null); + return; + } + + getUserMediaImpl.getUserMedia(constraints, result, mediaStream); + } + + public void getDisplayMedia(ConstraintsMap constraints, Result result) { + String streamId = getNextStreamUUID(); + MediaStream mediaStream = mFactory.createLocalMediaStream(streamId); + + if (mediaStream == null) { + // XXX The following does not follow the getUserMedia() algorithm + // specified by + // https://www.w3.org/TR/mediacapture-streams/#dom-mediadevices-getusermedia + // with respect to distinguishing the various causes of failure. + result.error( + /* type */ "getDisplayMedia", + "Failed to create new media stream", null); + return; + } + + getUserMediaImpl.getDisplayMedia(constraints, result, mediaStream); + } + + public void getSources(Result result) { + ConstraintsArray array = new ConstraintsArray(); + String[] names = new String[Camera.getNumberOfCameras()]; + + for (int i = 0; i < Camera.getNumberOfCameras(); ++i) { + ConstraintsMap info = getCameraInfo(i); + if (info != null) { + array.pushMap(info); + } + } + + ConstraintsMap audio = new ConstraintsMap(); + audio.putString("label", "Audio"); + audio.putString("deviceId", "audio-1"); + audio.putString("facing", ""); + audio.putString("kind", "audioinput"); + array.pushMap(audio); + result.success(array); + } + + private void createLocalMediaStream(Result result) { + String streamId = getNextStreamUUID(); + MediaStream mediaStream = mFactory.createLocalMediaStream(streamId); + localStreams.put(streamId, mediaStream); + + if (mediaStream == null) { + result.error(/* type */ "createLocalMediaStream", "Failed to create new media stream", null); + return; + } + Map resultMap = new HashMap<>(); + resultMap.put("streamId", mediaStream.getId()); + result.success(resultMap); + } + + public void mediaStreamTrackStop(final String id) { + // Is this functionality equivalent to `mediaStreamTrackRelease()` ? + // if so, we should merge this two and remove track from stream as well. + MediaStreamTrack track = localTracks.get(id); + if (track == null) { + Log.d(TAG, "mediaStreamTrackStop() track is null"); + return; + } + track.setEnabled(false); + if (track.kind().equals("video")) { + getUserMediaImpl.removeVideoCapturer(id); + } + localTracks.remove(id); + // What exactly does `detached` mean in doc? + // see: https://www.w3.org/TR/mediacapture-streams/#track-detached + } + + public void mediaStreamTrackSetEnabled(final String id, final boolean enabled) { + MediaStreamTrack track = localTracks.get(id); + if (track == null) { + Log.d(TAG, "mediaStreamTrackSetEnabled() track is null"); + return; + } else if (track.enabled() == enabled) { + return; + } + track.setEnabled(enabled); + } + + public void mediaStreamTrackSetVolume(final String id, final double volume) { + MediaStreamTrack track = localTracks.get(id); + if (track != null && track instanceof AudioTrack) { + Log.d(TAG, "setVolume(): " + id + "," + volume); + try { + ((AudioTrack) track).setVolume(volume); + } catch (Exception e) { + Log.e(TAG, "setVolume(): error", e); + } + } else { + Log.w(TAG, "setVolume(): track not found: " + id); + } + } + + public void mediaStreamAddTrack(final String streaemId, final String trackId, Result result) { + MediaStream mediaStream = localStreams.get(streaemId); + if (mediaStream != null) { + MediaStreamTrack track = localTracks.get(trackId); + if (track != null) { + if (track.kind().equals("audio")) { + mediaStream.addTrack((AudioTrack) track); + } else if (track.kind().equals("video")) { + mediaStream.addTrack((VideoTrack) track); + } + } else { + String errorMsg = "mediaStreamAddTrack() track [" + trackId + "] is null"; + Log.d(TAG, errorMsg); + result.error("mediaStreamAddTrack", errorMsg, null); + } + } else { + String errorMsg = "mediaStreamAddTrack() stream [" + trackId + "] is null"; + Log.d(TAG, errorMsg); + result.error("mediaStreamAddTrack", errorMsg, null); + } + result.success(null); + } + + public void mediaStreamRemoveTrack(final String streaemId, final String trackId, Result result) { + MediaStream mediaStream = localStreams.get(streaemId); + if (mediaStream != null) { + MediaStreamTrack track = localTracks.get(trackId); + if (track != null) { + if (track.kind().equals("audio")) { + mediaStream.removeTrack((AudioTrack) track); + } else if (track.kind().equals("video")) { + mediaStream.removeTrack((VideoTrack) track); + } + } else { + String errorMsg = "mediaStreamRemoveTrack() track [" + trackId + "] is null"; + Log.d(TAG, errorMsg); + result.error("mediaStreamRemoveTrack", errorMsg, null); + } + } else { + String errorMsg = "mediaStreamRemoveTrack() stream [" + trackId + "] is null"; + Log.d(TAG, errorMsg); + result.error("mediaStreamRemoveTrack", errorMsg, null); + } + result.success(null); + } + + public void mediaStreamTrackRelease(final String streamId, final String _trackId) { + MediaStream stream = localStreams.get(streamId); + if (stream == null) { + Log.d(TAG, "mediaStreamTrackRelease() stream is null"); + return; + } + MediaStreamTrack track = localTracks.get(_trackId); + if (track == null) { + Log.d(TAG, "mediaStreamTrackRelease() track is null"); + return; + } + track.setEnabled(false); // should we do this? + localTracks.remove(_trackId); + if (track.kind().equals("audio")) { + stream.removeTrack((AudioTrack) track); + } else if (track.kind().equals("video")) { + stream.removeTrack((VideoTrack) track); + getUserMediaImpl.removeVideoCapturer(_trackId); + } + } + + public ConstraintsMap getCameraInfo(int index) { + CameraInfo info = new CameraInfo(); + + try { + Camera.getCameraInfo(index, info); + } catch (Exception e) { + Logging.e("CameraEnumerationAndroid", "getCameraInfo failed on index " + index, e); + return null; + } + ConstraintsMap params = new ConstraintsMap(); + String facing = info.facing == 1 ? "front" : "back"; + params.putString("label", + "Camera " + index + ", Facing " + facing + ", Orientation " + info.orientation); + params.putString("deviceId", "" + index); + params.putString("facing", facing); + params.putString("kind", "videoinput"); + return params; + } + + private MediaConstraints defaultConstraints() { + MediaConstraints constraints = new MediaConstraints(); + // TODO video media + constraints.mandatory.add(new KeyValuePair("OfferToReceiveAudio", "true")); + constraints.mandatory.add(new KeyValuePair("OfferToReceiveVideo", "true")); + constraints.optional.add(new KeyValuePair("DtlsSrtpKeyAgreement", "true")); + return constraints; + } + + public void peerConnectionSetConfiguration(ConstraintsMap configuration, + PeerConnection peerConnection) { + if (peerConnection == null) { + Log.d(TAG, "peerConnectionSetConfiguration() peerConnection is null"); + return; + } + peerConnection.setConfiguration(parseRTCConfiguration(configuration)); + } + + public void peerConnectionAddStream(final String streamId, final String id, Result result) { + MediaStream mediaStream = localStreams.get(streamId); + if (mediaStream == null) { + Log.d(TAG, "peerConnectionAddStream() mediaStream is null"); + return; + } + PeerConnection peerConnection = getPeerConnection(id); + if (peerConnection != null) { + boolean res = peerConnection.addStream(mediaStream); + Log.d(TAG, "addStream" + result); + result.success(res); + } else { + Log.d(TAG, "peerConnectionAddStream() peerConnection is null"); + result.error("peerConnectionAddStreamFailed", + "peerConnectionAddStream() peerConnection is null", null); + } + } + + public void peerConnectionRemoveStream(final String streamId, final String id, Result result) { + MediaStream mediaStream = localStreams.get(streamId); + if (mediaStream == null) { + Log.d(TAG, "peerConnectionRemoveStream() mediaStream is null"); + return; + } + PeerConnection peerConnection = getPeerConnection(id); + if (peerConnection != null) { + peerConnection.removeStream(mediaStream); + result.success(null); + } else { + Log.d(TAG, "peerConnectionRemoveStream() peerConnection is null"); + result.error("peerConnectionRemoveStreamFailed", + "peerConnectionAddStream() peerConnection is null", null); + } + } + + public void peerConnectionCreateOffer( + String id, + ConstraintsMap constraints, + final Result result) { + PeerConnection peerConnection = getPeerConnection(id); + + if (peerConnection != null) { + peerConnection.createOffer(new SdpObserver() { + @Override + public void onCreateFailure(String s) { + result.error("WEBRTC_CREATE_OFFER_ERROR", s, null); + } + + @Override + public void onCreateSuccess(final SessionDescription sdp) { + ConstraintsMap params = new ConstraintsMap(); + params.putString("sdp", sdp.description); + params.putString("type", sdp.type.canonicalForm()); + result.success(params.toMap()); + } + + @Override + public void onSetFailure(String s) { + } + + @Override + public void onSetSuccess() { + } + }, parseMediaConstraints(constraints)); + } else { + Log.d(TAG, "peerConnectionCreateOffer() peerConnection is null"); + result.error("WEBRTC_CREATE_OFFER_ERROR", "peerConnection is null", null); + } + } + + public void peerConnectionCreateAnswer( + String id, + ConstraintsMap constraints, + final Result result) { + PeerConnection peerConnection = getPeerConnection(id); + + if (peerConnection != null) { + peerConnection.createAnswer(new SdpObserver() { + @Override + public void onCreateFailure(String s) { + result.error("WEBRTC_CREATE_ANSWER_ERROR", s, null); + } + + @Override + public void onCreateSuccess(final SessionDescription sdp) { + ConstraintsMap params = new ConstraintsMap(); + params.putString("sdp", sdp.description); + params.putString("type", sdp.type.canonicalForm()); + result.success(params.toMap()); + } + + @Override + public void onSetFailure(String s) { + } + + @Override + public void onSetSuccess() { + } + }, parseMediaConstraints(constraints)); + } else { + Log.d(TAG, "peerConnectionCreateAnswer() peerConnection is null"); + result.error("WEBRTC_CREATE_ANSWER_ERROR", "peerConnection is null", null); + } + } + + public void peerConnectionSetLocalDescription(ConstraintsMap sdpMap, final String id, + final Result result) { + PeerConnection peerConnection = getPeerConnection(id); + + Log.d(TAG, "peerConnectionSetLocalDescription() start"); + if (peerConnection != null) { + SessionDescription sdp = new SessionDescription( + Type.fromCanonicalForm(sdpMap.getString("type")), + sdpMap.getString("sdp") + ); + + peerConnection.setLocalDescription(new SdpObserver() { + @Override + public void onCreateSuccess(final SessionDescription sdp) { + } + + @Override + public void onSetSuccess() { + result.success(null); + } + + @Override + public void onCreateFailure(String s) { + } + + @Override + public void onSetFailure(String s) { + result.error("WEBRTC_SET_LOCAL_DESCRIPTION_ERROR", s, null); + } + }, sdp); + } else { + Log.d(TAG, "peerConnectionSetLocalDescription() peerConnection is null"); + result.error("WEBRTC_SET_LOCAL_DESCRIPTION_ERROR", "peerConnection is null", null); + } + Log.d(TAG, "peerConnectionSetLocalDescription() end"); + } + + public void peerConnectionSetRemoteDescription(final ConstraintsMap sdpMap, final String id, + final Result result) { + PeerConnection peerConnection = getPeerConnection(id); + // final String d = sdpMap.getString("type"); + + Log.d(TAG, "peerConnectionSetRemoteDescription() start"); + if (peerConnection != null) { + SessionDescription sdp = new SessionDescription( + Type.fromCanonicalForm(sdpMap.getString("type")), + sdpMap.getString("sdp") + ); + + peerConnection.setRemoteDescription(new SdpObserver() { + @Override + public void onCreateSuccess(final SessionDescription sdp) { + } + + @Override + public void onSetSuccess() { + result.success(null); + } + + @Override + public void onCreateFailure(String s) { + } + + @Override + public void onSetFailure(String s) { + result.error("WEBRTC_SET_REMOTE_DESCRIPTION_ERROR", s, null); + } + }, sdp); + } else { + Log.d(TAG, "peerConnectionSetRemoteDescription() peerConnection is null"); + result.error("WEBRTC_SET_REMOTE_DESCRIPTION_ERROR", "peerConnection is null", null); + } + Log.d(TAG, "peerConnectionSetRemoteDescription() end"); + } + + public void peerConnectionAddICECandidate(ConstraintsMap candidateMap, final String id, + final Result result) { + boolean res = false; + PeerConnection peerConnection = getPeerConnection(id); + Log.d(TAG, "peerConnectionAddICECandidate() start"); + if (peerConnection != null) { + IceCandidate candidate = new IceCandidate( + candidateMap.getString("sdpMid"), + candidateMap.getInt("sdpMLineIndex"), + candidateMap.getString("candidate") + ); + res = peerConnection.addIceCandidate(candidate); + } else { + Log.d(TAG, "peerConnectionAddICECandidate() peerConnection is null"); + result.error("peerConnectionAddICECandidateFailed", + "peerConnectionAddICECandidate() peerConnection is null", null); + } + result.success(res); + Log.d(TAG, "peerConnectionAddICECandidate() end"); + } + + public void peerConnectionGetStats(String trackId, String id, final Result result) { + PeerConnectionObserver pco = mPeerConnectionObservers.get(id); + if (pco == null || pco.getPeerConnection() == null) { + Log.d(TAG, "peerConnectionGetStats() peerConnection is null"); + } else { + pco.getStats(trackId, result); + } + } + + public void peerConnectionClose(final String id) { + PeerConnectionObserver pco = mPeerConnectionObservers.get(id); + if (pco == null || pco.getPeerConnection() == null) { + Log.d(TAG, "peerConnectionClose() peerConnection is null"); + } else { + pco.close(); + } + } + + public void peerConnectionDispose(final String id) { + PeerConnectionObserver pco = mPeerConnectionObservers.get(id); + if (pco == null || pco.getPeerConnection() == null) { + Log.d(TAG, "peerConnectionDispose() peerConnection is null"); + } else { + pco.dispose(); + mPeerConnectionObservers.remove(id); + } + if (mPeerConnectionObservers.size() == 0) { + audioManager.onAudioManagerRequested(false); + } + } + + public void mediaStreamRelease(final String id) { + MediaStream mediaStream = localStreams.get(id); + if (mediaStream != null) { + for (VideoTrack track : mediaStream.videoTracks) { + localTracks.remove(track.id()); + getUserMediaImpl.removeVideoCapturer(track.id()); + } + for (AudioTrack track : mediaStream.audioTracks) { + localTracks.remove(track.id()); + } + localStreams.remove(id); + } else { + Log.d(TAG, "mediaStreamRelease() mediaStream is null"); + } + } + + public void createDataChannel(final String peerConnectionId, String label, ConstraintsMap config, + Result result) { + // Forward to PeerConnectionObserver which deals with DataChannels + // because DataChannel is owned by PeerConnection. + PeerConnectionObserver pco + = mPeerConnectionObservers.get(peerConnectionId); + if (pco == null || pco.getPeerConnection() == null) { + Log.d(TAG, "createDataChannel() peerConnection is null"); + } else { + pco.createDataChannel(label, config, result); + } + } + + public void dataChannelSend(String peerConnectionId, int dataChannelId, ByteBuffer bytebuffer, + Boolean isBinary) { + // Forward to PeerConnectionObserver which deals with DataChannels + // because DataChannel is owned by PeerConnection. + PeerConnectionObserver pco + = mPeerConnectionObservers.get(peerConnectionId); + if (pco == null || pco.getPeerConnection() == null) { + Log.d(TAG, "dataChannelSend() peerConnection is null"); + } else { + pco.dataChannelSend(dataChannelId, bytebuffer, isBinary); + } + } + + public void dataChannelClose(String peerConnectionId, int dataChannelId) { + // Forward to PeerConnectionObserver which deals with DataChannels + // because DataChannel is owned by PeerConnection. + PeerConnectionObserver pco + = mPeerConnectionObservers.get(peerConnectionId); + if (pco == null || pco.getPeerConnection() == null) { + Log.d(TAG, "dataChannelClose() peerConnection is null"); + } else { + pco.dataChannelClose(dataChannelId); + } + } + + public void setActivity(Activity activity) { + this.activity = activity; + } +} diff --git a/android/src/main/java/com/cloudwebrtc/webrtc/PeerConnectionObserver.java b/android/src/main/java/com/cloudwebrtc/webrtc/PeerConnectionObserver.java index 9c2d141b61..532718ac7b 100755 --- a/android/src/main/java/com/cloudwebrtc/webrtc/PeerConnectionObserver.java +++ b/android/src/main/java/com/cloudwebrtc/webrtc/PeerConnectionObserver.java @@ -1,20 +1,18 @@ package com.cloudwebrtc.webrtc; -import java.io.UnsupportedEncodingException; -import java.nio.ByteBuffer; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -import android.util.Base64; import android.util.Log; import android.util.SparseArray; import androidx.annotation.Nullable; - import com.cloudwebrtc.webrtc.utils.AnyThreadSink; import com.cloudwebrtc.webrtc.utils.ConstraintsArray; import com.cloudwebrtc.webrtc.utils.ConstraintsMap; - +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodChannel.Result; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; import org.webrtc.AudioTrack; import org.webrtc.DataChannel; import org.webrtc.IceCandidate; @@ -26,453 +24,448 @@ import org.webrtc.StatsReport; import org.webrtc.VideoTrack; -import io.flutter.plugin.common.EventChannel; -import io.flutter.plugin.common.MethodChannel.Result; - class PeerConnectionObserver implements PeerConnection.Observer, EventChannel.StreamHandler { - private final static String TAG = FlutterWebRTCPlugin.TAG; - - private final SparseArray dataChannels - = new SparseArray(); - private final String id; - private PeerConnection peerConnection; - final Map remoteStreams; - final Map remoteTracks; - private final FlutterWebRTCPlugin plugin; - - EventChannel eventChannel; - EventChannel.EventSink eventSink; - - PeerConnectionObserver(FlutterWebRTCPlugin plugin, String id) { - this.plugin = plugin; - this.id = id; - this.remoteStreams = new HashMap(); - this.remoteTracks = new HashMap(); - - - this.eventChannel = - new EventChannel( - plugin.registrar().messenger(), - "FlutterWebRTC/peerConnectoinEvent" + id); - eventChannel.setStreamHandler(this); - this.eventSink = null; - } - - @Override - public void onListen(Object o, EventChannel.EventSink sink) { - eventSink = new AnyThreadSink(sink); - } - @Override - public void onCancel(Object o) { - eventSink = null; + private final static String TAG = FlutterWebRTCPlugin.TAG; + + private final SparseArray dataChannels = new SparseArray<>(); + private BinaryMessenger messenger; + private final String id; + private PeerConnection peerConnection; + final Map remoteStreams = new HashMap<>(); + final Map remoteTracks = new HashMap<>(); + private final StateProvider stateProvider; + + private final EventChannel eventChannel; + private EventChannel.EventSink eventSink; + + PeerConnectionObserver(StateProvider stateProvider, BinaryMessenger messenger, String id) { + this.stateProvider = stateProvider; + this.messenger = messenger; + this.id = id; + + eventChannel = new EventChannel(messenger, "FlutterWebRTC/peerConnectoinEvent" + id); + eventChannel.setStreamHandler(this); + } + + @Override + public void onListen(Object o, EventChannel.EventSink sink) { + eventSink = new AnyThreadSink(sink); + } + + @Override + public void onCancel(Object o) { + eventSink = null; + } + + PeerConnection getPeerConnection() { + return peerConnection; + } + + void setPeerConnection(PeerConnection peerConnection) { + this.peerConnection = peerConnection; + } + + void close() { + peerConnection.close(); + remoteStreams.clear(); + remoteTracks.clear(); + dataChannels.clear(); + } + + void dispose() { + this.close(); + + peerConnection.dispose(); + eventChannel.setStreamHandler(null); + } + + void createDataChannel(String label, ConstraintsMap config, Result result) { + DataChannel.Init init = new DataChannel.Init(); + if (config != null) { + if (config.hasKey("id")) { + init.id = config.getInt("id"); + } + if (config.hasKey("ordered")) { + init.ordered = config.getBoolean("ordered"); + } + if (config.hasKey("maxRetransmitTime")) { + init.maxRetransmitTimeMs = config.getInt("maxRetransmitTime"); + } + if (config.hasKey("maxRetransmits")) { + init.maxRetransmits = config.getInt("maxRetransmits"); + } + if (config.hasKey("protocol")) { + init.protocol = config.getString("protocol"); + } + if (config.hasKey("negotiated")) { + init.negotiated = config.getBoolean("negotiated"); + } } - - PeerConnection getPeerConnection() { - return peerConnection; + DataChannel dataChannel = peerConnection.createDataChannel(label, init); + // XXX RTP data channels are not defined by the WebRTC standard, have + // been deprecated in Chromium, and Google have decided (in 2015) to no + // longer support them (in the face of multiple reported issues of + // breakages). + int dataChannelId = init.id; + if (dataChannel != null && -1 != dataChannelId) { + dataChannels.put(dataChannelId, dataChannel); + registerDataChannelObserver(dataChannelId, dataChannel); + + ConstraintsMap params = new ConstraintsMap(); + params.putInt("id", dataChannel.id()); + params.putString("label", dataChannel.label()); + result.success(params.toMap()); + } else { + result.error("createDataChannel", + "Can't create data-channel for id: " + dataChannelId, + null); } - - void setPeerConnection(PeerConnection peerConnection) { - this.peerConnection = peerConnection; + } + + void dataChannelClose(int dataChannelId) { + DataChannel dataChannel = dataChannels.get(dataChannelId); + if (dataChannel != null) { + dataChannel.close(); + dataChannels.remove(dataChannelId); + } else { + Log.d(TAG, "dataChannelClose() dataChannel is null"); } - - void close() { - peerConnection.close(); - remoteStreams.clear(); - remoteTracks.clear(); - dataChannels.clear(); + } + + void dataChannelSend(int dataChannelId, ByteBuffer byteBuffer, Boolean isBinary) { + DataChannel dataChannel = dataChannels.get(dataChannelId); + if (dataChannel != null) { + DataChannel.Buffer buffer = new DataChannel.Buffer(byteBuffer, isBinary); + dataChannel.send(buffer); + } else { + Log.d(TAG, "dataChannelSend() dataChannel is null"); } + } + + void getStats(String trackId, final Result result) { + MediaStreamTrack track = null; + if (trackId == null + || trackId.isEmpty() + || (track = stateProvider.getLocalTracks().get(trackId)) != null + || (track = remoteTracks.get(trackId)) != null) { + peerConnection.getStats( + new StatsObserver() { + @Override + public void onComplete(StatsReport[] reports) { + + final int reportCount = reports.length; + ConstraintsMap params = new ConstraintsMap(); + ConstraintsArray stats = new ConstraintsArray(); + + for (int i = 0; i < reportCount; ++i) { + StatsReport report = reports[i]; + ConstraintsMap report_map = new ConstraintsMap(); + + report_map.putString("id", report.id); + report_map.putString("type", report.type); + report_map.putDouble("timestamp", report.timestamp); + + StatsReport.Value[] values = report.values; + ConstraintsMap v_map = new ConstraintsMap(); + final int valueCount = values.length; + for (int j = 0; j < valueCount; ++j) { + StatsReport.Value v = values[j]; + v_map.putString(v.name, v.value); + } - void dispose() { - this.close(); - peerConnection.dispose(); - eventChannel.setStreamHandler(null); - } + report_map.putMap("values", v_map.toMap()); + stats.pushMap(report_map); + } - void createDataChannel(String label, ConstraintsMap config, Result result) { - DataChannel.Init init = new DataChannel.Init(); - if (config != null) { - if (config.hasKey("id")) { - init.id = config.getInt("id"); - } - if (config.hasKey("ordered")) { - init.ordered = config.getBoolean("ordered"); - } - if (config.hasKey("maxRetransmitTime")) { - init.maxRetransmitTimeMs = config.getInt("maxRetransmitTime"); - } - if (config.hasKey("maxRetransmits")) { - init.maxRetransmits = config.getInt("maxRetransmits"); - } - if (config.hasKey("protocol")) { - init.protocol = config.getString("protocol"); - } - if (config.hasKey("negotiated")) { - init.negotiated = config.getBoolean("negotiated"); + params.putArray("stats", stats.toArrayList()); + result.success(params.toMap()); } - } - DataChannel dataChannel = peerConnection.createDataChannel(label, init); - // XXX RTP data channels are not defined by the WebRTC standard, have - // been deprecated in Chromium, and Google have decided (in 2015) to no - // longer support them (in the face of multiple reported issues of - // breakages). - int dataChannelId = init.id; - if (dataChannel != null && -1 != dataChannelId) { - dataChannels.put(dataChannelId, dataChannel); - registerDataChannelObserver(dataChannelId, dataChannel); - - ConstraintsMap params = new ConstraintsMap(); - params.putInt("id", dataChannel.id()); - params.putString("label", dataChannel.label()); - result.success(params.toMap()); - }else{ - result.error("createDataChannel", - "Can't create data-channel for id: " + dataChannelId, - null); - } + }, + track); + } else { + Log.e(TAG, "peerConnectionGetStats() MediaStreamTrack not found for id: " + trackId); + result.error("peerConnectionGetStats", + "peerConnectionGetStats() MediaStreamTrack not found for id: " + trackId, + null); } - - void dataChannelClose(int dataChannelId) { - DataChannel dataChannel = dataChannels.get(dataChannelId); - if (dataChannel != null) { - dataChannel.close(); - dataChannels.remove(dataChannelId); - } else { - Log.d(TAG, "dataChannelClose() dataChannel is null"); - } + } + + @Override + public void onIceCandidate(final IceCandidate candidate) { + Log.d(TAG, "onIceCandidate"); + ConstraintsMap params = new ConstraintsMap(); + params.putString("event", "onCandidate"); + ConstraintsMap candidateParams = new ConstraintsMap(); + candidateParams.putInt("sdpMLineIndex", candidate.sdpMLineIndex); + candidateParams.putString("sdpMid", candidate.sdpMid); + candidateParams.putString("candidate", candidate.sdp); + params.putMap("candidate", candidateParams.toMap()); + sendEvent(params); + } + + @Override + public void onIceCandidatesRemoved(final IceCandidate[] candidates) { + Log.d(TAG, "onIceCandidatesRemoved"); + } + + @Override + public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) { + ConstraintsMap params = new ConstraintsMap(); + params.putString("event", "iceConnectionState"); + params.putString("state", iceConnectionStateString(iceConnectionState)); + sendEvent(params); + } + + @Override + public void onIceConnectionReceivingChange(boolean var1) { + } + + @Override + public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) { + Log.d(TAG, "onIceGatheringChange" + iceGatheringState.name()); + ConstraintsMap params = new ConstraintsMap(); + params.putString("event", "iceGatheringState"); + params.putString("state", iceGatheringStateString(iceGatheringState)); + sendEvent(params); + } + + private String getUIDForStream(MediaStream mediaStream) { + for (Iterator> i + = remoteStreams.entrySet().iterator(); + i.hasNext(); ) { + Map.Entry e = i.next(); + if (e.getValue().equals(mediaStream)) { + return e.getKey(); + } } - - void dataChannelSend(int dataChannelId, ByteBuffer byteBuffer, Boolean isBinary) { - DataChannel dataChannel = dataChannels.get(dataChannelId); - if (dataChannel != null) { - DataChannel.Buffer buffer = new DataChannel.Buffer(byteBuffer, isBinary); - dataChannel.send(buffer); - } else { - Log.d(TAG, "dataChannelSend() dataChannel is null"); + return null; + } + + @Override + public void onAddStream(MediaStream mediaStream) { + String streamUID = null; + String streamId = mediaStream.getId(); + // The native WebRTC implementation has a special concept of a default + // MediaStream instance with the label default that the implementation + // reuses. + if ("default".equals(streamId)) { + for (Map.Entry e + : remoteStreams.entrySet()) { + if (e.getValue().equals(mediaStream)) { + streamUID = e.getKey(); + break; } + } } - void getStats(String trackId, final Result result) { - MediaStreamTrack track = null; - if (trackId == null - || trackId.isEmpty() - || (track = plugin.localTracks.get(trackId)) != null - || (track = remoteTracks.get(trackId)) != null) { - peerConnection.getStats( - new StatsObserver() { - @Override - public void onComplete(StatsReport[] reports) { - - final int reportCount = reports.length; - ConstraintsMap params = new ConstraintsMap(); - ConstraintsArray stats = new ConstraintsArray(); - - for (int i = 0; i < reportCount; ++i) { - StatsReport report = reports[i]; - ConstraintsMap report_map = new ConstraintsMap(); - - report_map.putString("id", report.id); - report_map.putString("type", report.type); - report_map.putDouble("timestamp", report.timestamp); - - StatsReport.Value[] values = report.values; - ConstraintsMap v_map = new ConstraintsMap(); - final int valueCount = values.length; - for (int j = 0; j < valueCount; ++j) { - StatsReport.Value v = values[j]; - v_map.putString(v.name, v.value); - } - - report_map.putMap("values", v_map.toMap()); - stats.pushMap(report_map); - } - - params.putArray("stats", stats.toArrayList()); - result.success(params.toMap()); - } - }, - track); - } else { - Log.e(TAG, "peerConnectionGetStats() MediaStreamTrack not found for id: " + trackId); - result.error("peerConnectionGetStats", - "peerConnectionGetStats() MediaStreamTrack not found for id: " + trackId, - null); - } + if (streamUID == null) { + streamUID = stateProvider.getNextStreamUUID(); + remoteStreams.put(streamId, mediaStream); } - @Override - public void onIceCandidate(final IceCandidate candidate) { - Log.d(TAG, "onIceCandidate"); - ConstraintsMap params = new ConstraintsMap(); - params.putString("event", "onCandidate"); - ConstraintsMap candidateParams = new ConstraintsMap(); - candidateParams.putInt("sdpMLineIndex", candidate.sdpMLineIndex); - candidateParams.putString("sdpMid", candidate.sdpMid); - candidateParams.putString("candidate", candidate.sdp); - params.putMap("candidate", candidateParams.toMap()); - sendEvent(params); - } + ConstraintsMap params = new ConstraintsMap(); + params.putString("event", "onAddStream"); + params.putString("streamId", streamId); - @Override - public void onIceCandidatesRemoved(final IceCandidate[] candidates) { - Log.d(TAG, "onIceCandidatesRemoved"); - } + ConstraintsArray audioTracks = new ConstraintsArray(); + ConstraintsArray videoTracks = new ConstraintsArray(); - @Override - public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) { - ConstraintsMap params = new ConstraintsMap(); - params.putString("event", "iceConnectionState"); - params.putString("state", iceConnectionStateString(iceConnectionState)); - sendEvent(params); - } + for (int i = 0; i < mediaStream.videoTracks.size(); i++) { + VideoTrack track = mediaStream.videoTracks.get(i); + String trackId = track.id(); - @Override - public void onIceConnectionReceivingChange(boolean var1) { - } + remoteTracks.put(trackId, track); - @Override - public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) { - Log.d(TAG, "onIceGatheringChange" + iceGatheringState.name()); - ConstraintsMap params = new ConstraintsMap(); - params.putString("event", "iceGatheringState"); - params.putString("state", iceGatheringStateString(iceGatheringState)); - sendEvent(params); + ConstraintsMap trackInfo = new ConstraintsMap(); + trackInfo.putString("id", trackId); + trackInfo.putString("label", "Video"); + trackInfo.putString("kind", track.kind()); + trackInfo.putBoolean("enabled", track.enabled()); + trackInfo.putString("readyState", track.state().toString()); + trackInfo.putBoolean("remote", true); + videoTracks.pushMap(trackInfo); } - - private String getUIDForStream(MediaStream mediaStream) { - for (Iterator> i - = remoteStreams.entrySet().iterator(); - i.hasNext();) { - Map.Entry e = i.next(); - if (e.getValue().equals(mediaStream)) { - return e.getKey(); - } - } - return null; + for (int i = 0; i < mediaStream.audioTracks.size(); i++) { + AudioTrack track = mediaStream.audioTracks.get(i); + String trackId = track.id(); + + remoteTracks.put(trackId, track); + + ConstraintsMap trackInfo = new ConstraintsMap(); + trackInfo.putString("id", trackId); + trackInfo.putString("label", "Audio"); + trackInfo.putString("kind", track.kind()); + trackInfo.putBoolean("enabled", track.enabled()); + trackInfo.putString("readyState", track.state().toString()); + trackInfo.putBoolean("remote", true); + audioTracks.pushMap(trackInfo); } + params.putArray("audioTracks", audioTracks.toArrayList()); + params.putArray("videoTracks", videoTracks.toArrayList()); - @Override - public void onAddStream(MediaStream mediaStream) { - String streamUID = null; - String streamId = mediaStream.getId(); - // The native WebRTC implementation has a special concept of a default - // MediaStream instance with the label default that the implementation - // reuses. - if ("default".equals(streamId)) { - for (Map.Entry e - : remoteStreams.entrySet()) { - if (e.getValue().equals(mediaStream)) { - streamUID = e.getKey(); - break; - } - } - } - - if (streamUID == null){ - streamUID = plugin.getNextStreamUUID(); - remoteStreams.put(streamId, mediaStream); - } - - ConstraintsMap params = new ConstraintsMap(); - params.putString("event", "onAddStream"); - params.putString("streamId", streamId); + sendEvent(params); + } - ConstraintsArray audioTracks = new ConstraintsArray(); - ConstraintsArray videoTracks = new ConstraintsArray(); - for (int i = 0; i < mediaStream.videoTracks.size(); i++) { - VideoTrack track = mediaStream.videoTracks.get(i); - String trackId = track.id(); - - remoteTracks.put(trackId, track); - - ConstraintsMap trackInfo = new ConstraintsMap(); - trackInfo.putString("id", trackId); - trackInfo.putString("label", "Video"); - trackInfo.putString("kind", track.kind()); - trackInfo.putBoolean("enabled", track.enabled()); - trackInfo.putString("readyState", track.state().toString()); - trackInfo.putBoolean("remote", true); - videoTracks.pushMap(trackInfo); - } - for (int i = 0; i < mediaStream.audioTracks.size(); i++) { - AudioTrack track = mediaStream.audioTracks.get(i); - String trackId = track.id(); - - remoteTracks.put(trackId, track); - - ConstraintsMap trackInfo = new ConstraintsMap(); - trackInfo.putString("id", trackId); - trackInfo.putString("label", "Audio"); - trackInfo.putString("kind", track.kind()); - trackInfo.putBoolean("enabled", track.enabled()); - trackInfo.putString("readyState", track.state().toString()); - trackInfo.putBoolean("remote", true); - audioTracks.pushMap(trackInfo); - } - params.putArray("audioTracks", audioTracks.toArrayList()); - params.putArray("videoTracks", videoTracks.toArrayList()); - - sendEvent(params); + void sendEvent(ConstraintsMap event) { + if (eventSink != null) { + eventSink.success(event.toMap()); } + } + @Override + public void onRemoveStream(MediaStream mediaStream) { - void sendEvent(ConstraintsMap event) { - if(eventSink != null ) - eventSink.success(event.toMap()); - } - - @Override - public void onRemoveStream(MediaStream mediaStream) { + String streamId = mediaStream.getId(); - String streamId = mediaStream.getId(); - - for (VideoTrack track : mediaStream.videoTracks) { - this.remoteTracks.remove(track.id()); - } - for (AudioTrack track : mediaStream.audioTracks) { - this.remoteTracks.remove(track.id()); - } - - this.remoteStreams.remove(streamId); - ConstraintsMap params = new ConstraintsMap(); - params.putString("event", "onRemoveStream"); - params.putString("streamId", streamId); - sendEvent(params); - } - - @Override - public void onAddTrack(RtpReceiver receiver, MediaStream[] mediaStreams){ - Log.d(TAG, "onAddTrack"); - for (MediaStream stream : mediaStreams) { - String streamId = stream.getId(); - MediaStreamTrack track = receiver.track(); - ConstraintsMap params = new ConstraintsMap(); - params.putString("event", "onAddTrack"); - params.putString("streamId", streamId); - params.putString("trackId", track.id()); - - String trackId = track.id(); - ConstraintsMap trackInfo = new ConstraintsMap(); - trackInfo.putString("id", trackId); - trackInfo.putString("label", track.kind()); - trackInfo.putString("kind", track.kind()); - trackInfo.putBoolean("enabled", track.enabled()); - trackInfo.putString("readyState", track.state().toString()); - trackInfo.putBoolean("remote", true); - params.putMap("track", trackInfo.toMap()); - sendEvent(params); - } + for (VideoTrack track : mediaStream.videoTracks) { + this.remoteTracks.remove(track.id()); } - @Override - public void onDataChannel(DataChannel dataChannel) { - // XXX Unfortunately, the Java WebRTC API doesn't expose the id - // of the underlying C++/native DataChannel (even though the - // WebRTC standard defines the DataChannel.id property). As a - // workaround, generated an id which will surely not clash with - // the ids of the remotely-opened (and standard-compliant - // locally-opened) DataChannels. - int dataChannelId = -1; - // The RTCDataChannel.id space is limited to unsigned short by - // the standard: - // https://www.w3.org/TR/webrtc/#dom-datachannel-id. - // Additionally, 65535 is reserved due to SCTP INIT and - // INIT-ACK chunks only allowing a maximum of 65535 streams to - // be negotiated (as defined by the WebRTC Data Channel - // Establishment Protocol). - for (int i = 65536; i <= Integer.MAX_VALUE; ++i) { - if (null == dataChannels.get(i, null)) { - dataChannelId = i; - break; - } - } - if (-1 == dataChannelId) { - return; - } - ConstraintsMap params = new ConstraintsMap(); - params.putString("event", "didOpenDataChannel"); - params.putInt("id", dataChannelId); - params.putString("label", dataChannel.label()); - - dataChannels.put(dataChannelId, dataChannel); - registerDataChannelObserver(dataChannelId, dataChannel); - - sendEvent(params); + for (AudioTrack track : mediaStream.audioTracks) { + this.remoteTracks.remove(track.id()); } - private void registerDataChannelObserver(int dcId, DataChannel dataChannel) { - // DataChannel.registerObserver implementation does not allow to - // unregister, so the observer is registered here and is never - // unregistered - dataChannel.registerObserver( - new DataChannelObserver(plugin, id, dcId, dataChannel)); + this.remoteStreams.remove(streamId); + ConstraintsMap params = new ConstraintsMap(); + params.putString("event", "onRemoveStream"); + params.putString("streamId", streamId); + sendEvent(params); + } + + @Override + public void onAddTrack(RtpReceiver receiver, MediaStream[] mediaStreams) { + Log.d(TAG, "onAddTrack"); + for (MediaStream stream : mediaStreams) { + String streamId = stream.getId(); + MediaStreamTrack track = receiver.track(); + ConstraintsMap params = new ConstraintsMap(); + params.putString("event", "onAddTrack"); + params.putString("streamId", streamId); + params.putString("trackId", track.id()); + + String trackId = track.id(); + ConstraintsMap trackInfo = new ConstraintsMap(); + trackInfo.putString("id", trackId); + trackInfo.putString("label", track.kind()); + trackInfo.putString("kind", track.kind()); + trackInfo.putBoolean("enabled", track.enabled()); + trackInfo.putString("readyState", track.state().toString()); + trackInfo.putBoolean("remote", true); + params.putMap("track", trackInfo.toMap()); + sendEvent(params); } - - @Override - public void onRenegotiationNeeded() { - ConstraintsMap params = new ConstraintsMap(); - params.putString("event", "onRenegotiationNeeded"); - sendEvent(params); + } + + @Override + public void onDataChannel(DataChannel dataChannel) { + // XXX Unfortunately, the Java WebRTC API doesn't expose the id + // of the underlying C++/native DataChannel (even though the + // WebRTC standard defines the DataChannel.id property). As a + // workaround, generated an id which will surely not clash with + // the ids of the remotely-opened (and standard-compliant + // locally-opened) DataChannels. + int dataChannelId = -1; + // The RTCDataChannel.id space is limited to unsigned short by + // the standard: + // https://www.w3.org/TR/webrtc/#dom-datachannel-id. + // Additionally, 65535 is reserved due to SCTP INIT and + // INIT-ACK chunks only allowing a maximum of 65535 streams to + // be negotiated (as defined by the WebRTC Data Channel + // Establishment Protocol). + for (int i = 65536; i <= Integer.MAX_VALUE; ++i) { + if (null == dataChannels.get(i, null)) { + dataChannelId = i; + break; + } } - - @Override - public void onSignalingChange(PeerConnection.SignalingState signalingState) { - ConstraintsMap params = new ConstraintsMap(); - params.putString("event", "signalingState"); - params.putString("state", signalingStateString(signalingState)); - sendEvent(params); + if (-1 == dataChannelId) { + return; } - - @Nullable - private String iceConnectionStateString(PeerConnection.IceConnectionState iceConnectionState) { - switch (iceConnectionState) { - case NEW: - return "new"; - case CHECKING: - return "checking"; - case CONNECTED: - return "connected"; - case COMPLETED: - return "completed"; - case FAILED: - return "failed"; - case DISCONNECTED: - return "disconnected"; - case CLOSED: - return "closed"; - } - return null; + ConstraintsMap params = new ConstraintsMap(); + params.putString("event", "didOpenDataChannel"); + params.putInt("id", dataChannelId); + params.putString("label", dataChannel.label()); + + dataChannels.put(dataChannelId, dataChannel); + registerDataChannelObserver(dataChannelId, dataChannel); + + sendEvent(params); + } + + private void registerDataChannelObserver(int dcId, DataChannel dataChannel) { + // DataChannel.registerObserver implementation does not allow to + // unregister, so the observer is registered here and is never + // unregistered + dataChannel.registerObserver( + new DataChannelObserver(messenger, id, dcId, dataChannel)); + } + + @Override + public void onRenegotiationNeeded() { + ConstraintsMap params = new ConstraintsMap(); + params.putString("event", "onRenegotiationNeeded"); + sendEvent(params); + } + + @Override + public void onSignalingChange(PeerConnection.SignalingState signalingState) { + ConstraintsMap params = new ConstraintsMap(); + params.putString("event", "signalingState"); + params.putString("state", signalingStateString(signalingState)); + sendEvent(params); + } + + @Nullable + private String iceConnectionStateString(PeerConnection.IceConnectionState iceConnectionState) { + switch (iceConnectionState) { + case NEW: + return "new"; + case CHECKING: + return "checking"; + case CONNECTED: + return "connected"; + case COMPLETED: + return "completed"; + case FAILED: + return "failed"; + case DISCONNECTED: + return "disconnected"; + case CLOSED: + return "closed"; } - - @Nullable - private String iceGatheringStateString(PeerConnection.IceGatheringState iceGatheringState) { - switch (iceGatheringState) { - case NEW: - return "new"; - case GATHERING: - return "gathering"; - case COMPLETE: - return "complete"; - } - return null; + return null; + } + + @Nullable + private String iceGatheringStateString(PeerConnection.IceGatheringState iceGatheringState) { + switch (iceGatheringState) { + case NEW: + return "new"; + case GATHERING: + return "gathering"; + case COMPLETE: + return "complete"; } - - @Nullable - private String signalingStateString(PeerConnection.SignalingState signalingState) { - switch (signalingState) { - case STABLE: - return "stable"; - case HAVE_LOCAL_OFFER: - return "have-local-offer"; - case HAVE_LOCAL_PRANSWER: - return "have-local-pranswer"; - case HAVE_REMOTE_OFFER: - return "have-remote-offer"; - case HAVE_REMOTE_PRANSWER: - return "have-remote-pranswer"; - case CLOSED: - return "closed"; - } - return null; + return null; + } + + @Nullable + private String signalingStateString(PeerConnection.SignalingState signalingState) { + switch (signalingState) { + case STABLE: + return "stable"; + case HAVE_LOCAL_OFFER: + return "have-local-offer"; + case HAVE_LOCAL_PRANSWER: + return "have-local-pranswer"; + case HAVE_REMOTE_OFFER: + return "have-remote-offer"; + case HAVE_REMOTE_PRANSWER: + return "have-remote-pranswer"; + case CLOSED: + return "closed"; } + return null; + } } diff --git a/android/src/main/java/com/cloudwebrtc/webrtc/StateProvider.java b/android/src/main/java/com/cloudwebrtc/webrtc/StateProvider.java new file mode 100644 index 0000000000..6c0c9f3a5b --- /dev/null +++ b/android/src/main/java/com/cloudwebrtc/webrtc/StateProvider.java @@ -0,0 +1,29 @@ +package com.cloudwebrtc.webrtc; + +import android.app.Activity; +import androidx.annotation.Nullable; +import java.util.Map; +import org.webrtc.MediaStream; +import org.webrtc.MediaStreamTrack; +import org.webrtc.PeerConnectionFactory; + +/** + * Provides interested components with access to the current application state. + * + * It is encouraged to use this class instead of a component directly. + */ +public interface StateProvider { + + Map getLocalStreams(); + + Map getLocalTracks(); + + String getNextStreamUUID(); + + String getNextTrackUUID(); + + PeerConnectionFactory getPeerConnectionFactory(); + + @Nullable + Activity getActivity(); +} diff --git a/android/src/main/java/com/cloudwebrtc/webrtc/utils/MediaConstraintsUtils.java b/android/src/main/java/com/cloudwebrtc/webrtc/utils/MediaConstraintsUtils.java new file mode 100644 index 0000000000..ce41031a8c --- /dev/null +++ b/android/src/main/java/com/cloudwebrtc/webrtc/utils/MediaConstraintsUtils.java @@ -0,0 +1,92 @@ +package com.cloudwebrtc.webrtc.utils; + +import android.util.Log; +import java.util.List; +import java.util.Map.Entry; +import org.webrtc.MediaConstraints; +import org.webrtc.MediaConstraints.KeyValuePair; + +public class MediaConstraintsUtils { + + static public final String TAG = "MediaConstraintsUtils"; + + /** + * Parses mandatory and optional "GUM" constraints described by a specific + * ConstraintsMap. + * + * @param constraints A ConstraintsMap which represents a JavaScript object specifying + * the constraints to be parsed into a + * MediaConstraints instance. + * @return A new MediaConstraints instance initialized with the mandatory and optional + * constraint keys and values specified by + * constraints. + */ + public static MediaConstraints parseMediaConstraints(ConstraintsMap constraints) { + MediaConstraints mediaConstraints = new MediaConstraints(); + + if (constraints.hasKey("mandatory") + && constraints.getType("mandatory") == ObjectType.Map) { + parseConstraints(constraints.getMap("mandatory"), + mediaConstraints.mandatory); + } else { + Log.d(TAG, "mandatory constraints are not a map"); + } + + if (constraints.hasKey("optional") + && constraints.getType("optional") == ObjectType.Array) { + ConstraintsArray optional = constraints.getArray("optional"); + + for (int i = 0, size = optional.size(); i < size; i++) { + if (optional.getType(i) == ObjectType.Map) { + parseConstraints( + optional.getMap(i), + mediaConstraints.optional); + } + } + } else { + Log.d(TAG, "optional constraints are not an array"); + } + + return mediaConstraints; + } + + /** + * Parses a constraint set specified in the form of a JavaScript object into a specific + * List of MediaConstraints.KeyValuePairs. + * + * @param src The constraint set in the form of a JavaScript object to parse. + * @param dst The List of MediaConstraints.KeyValuePairs into which the + * specified src is to be parsed. + */ + private static void parseConstraints( + ConstraintsMap src, + List dst) { + + for (Entry entry : src.toMap().entrySet()) { + String key = entry.getKey(); + String value = getMapStrValue(src, entry.getKey()); + dst.add(new KeyValuePair(key, value)); + } + } + + private static String getMapStrValue(ConstraintsMap map, String key) { + if (!map.hasKey(key)) { + return null; + } + ObjectType type = map.getType(key); + switch (type) { + case Boolean: + return String.valueOf(map.getBoolean(key)); + case Number: + // Don't know how to distinguish between Int and Double from + // ReadableType.Number. 'getInt' will fail on double value, + // while 'getDouble' works for both. + // return String.valueOf(map.getInt(key)); + return String.valueOf(map.getDouble(key)); + case String: + return map.getString(key); + default: + return null; + } + } +} diff --git a/android/src/main/java/com/cloudwebrtc/webrtc/utils/PermissionUtils.java b/android/src/main/java/com/cloudwebrtc/webrtc/utils/PermissionUtils.java index d8c6216804..c86d9c3a1c 100755 --- a/android/src/main/java/com/cloudwebrtc/webrtc/utils/PermissionUtils.java +++ b/android/src/main/java/com/cloudwebrtc/webrtc/utils/PermissionUtils.java @@ -5,248 +5,208 @@ import android.app.FragmentTransaction; import android.content.pm.PackageManager; import android.os.Build; +import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.ResultReceiver; - -import com.cloudwebrtc.webrtc.FlutterWebRTCPlugin; - +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; import java.util.ArrayList; -/** - * Helper module for dealing with dynamic permissions, introduced in Android M - * (API level 23). - */ +/** Helper module for dealing with dynamic permissions, introduced in Android M (API level 23). */ public class PermissionUtils { - /** - * Constants for internal fields in the Bundle exchanged between - * the activity requesting the permissions and the auxiliary activity we - * spawn for this purpose. - */ - private static final String GRANT_RESULTS = "GRANT_RESULT"; - private static final String PERMISSIONS = "PERMISSION"; - private static final String REQUEST_CODE = "REQUEST_CODE"; - private static final String RESULT_RECEIVER = "RESULT_RECEIVER"; - - /** - * Incrementing counter for permission requests. Each request must have a - * unique numeric code. - */ - private static int requestCode; - - private static void requestPermissions( - FlutterWebRTCPlugin plugin, - String[] permissions, - ResultReceiver resultReceiver) { - // Ask the Context whether we have already been granted the requested - // permissions. - int size = permissions.length; - int[] grantResults = new int[size]; - boolean permissionsGranted = true; - - for (int i = 0; i < size; ++i) { - int grantResult; - // No need to ask for permission on pre-Marshmallow - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) - grantResult = PackageManager.PERMISSION_GRANTED; - else - grantResult = plugin.getContext().checkSelfPermission(permissions[i]); - - grantResults[i] = grantResult; - if (grantResult != PackageManager.PERMISSION_GRANTED) { - permissionsGranted = false; - } - } - - // Obviously, if the requested permissions have already been granted, - // there is nothing to ask the user about. On the other hand, if there - // is no Activity or the runtime permissions are not supported, there is - // no way to ask the user to grant us the denied permissions. - int requestCode = ++PermissionUtils.requestCode; - - if (permissionsGranted - // Here we test for the target SDK version with which *the app* - // was compiled. If we use Build.VERSION.SDK_INT that would give - // us the API version of the device itself, not the version the - // app was compiled for. When compiled for API level < 23 we - // must still use old permissions model, regardless of the - // Android version on the device. - || Build.VERSION.SDK_INT < Build.VERSION_CODES.M - || plugin.getActivity().getApplicationInfo().targetSdkVersion - < Build.VERSION_CODES.M) { - send(resultReceiver, requestCode, permissions, grantResults); - return; - } + /** + * Constants for internal fields in the Bundle exchanged between the activity requesting + * the permissions and the auxiliary activity we spawn for this purpose. + */ + private static final String GRANT_RESULTS = "GRANT_RESULT"; + + private static final String PERMISSIONS = "PERMISSION"; + private static final String REQUEST_CODE = "REQUEST_CODE"; + private static final String RESULT_RECEIVER = "RESULT_RECEIVER"; + + /** Incrementing counter for permission requests. Each request must have a unique numeric code. */ + private static int requestCode; + + private static void requestPermissions( + Activity activity, String[] permissions, ResultReceiver resultReceiver) { + // Ask the Context whether we have already been granted the requested + // permissions. + int size = permissions.length; + int[] grantResults = new int[size]; + boolean permissionsGranted = true; + + for (int i = 0; i < size; ++i) { + int grantResult; + // No need to ask for permission on pre-Marshmallow + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + grantResult = PackageManager.PERMISSION_GRANTED; + else grantResult = activity.checkSelfPermission(permissions[i]); + + grantResults[i] = grantResult; + if (grantResult != PackageManager.PERMISSION_GRANTED) { + permissionsGranted = false; + } + } - Activity activity = plugin.getActivity(); + // Obviously, if the requested permissions have already been granted, + // there is nothing to ask the user about. On the other hand, if there + // is no Activity or the runtime permissions are not supported, there is + // no way to ask the user to grant us the denied permissions. + int requestCode = ++PermissionUtils.requestCode; + + if (permissionsGranted + // Here we test for the target SDK version with which *the app* + // was compiled. If we use Build.VERSION.SDK_INT that would give + // us the API version of the device itself, not the version the + // app was compiled for. When compiled for API level < 23 we + // must still use old permissions model, regardless of the + // Android version on the device. + || Build.VERSION.SDK_INT < Build.VERSION_CODES.M + || activity.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.M) { + send(resultReceiver, requestCode, permissions, grantResults); + return; + } - if (activity == null) { - return; + Bundle args = new Bundle(); + args.putInt(REQUEST_CODE, requestCode); + args.putParcelable(RESULT_RECEIVER, resultReceiver); + args.putStringArray(PERMISSIONS, permissions); + + RequestPermissionsFragment fragment = new RequestPermissionsFragment(); + fragment.setArguments(args); + + FragmentTransaction transaction = + activity + .getFragmentManager() + .beginTransaction() + .add(fragment, fragment.getClass().getName() + "-" + requestCode); + + try { + transaction.commit(); + } catch (IllegalStateException ise) { + // Context is a Plugin, just send result back. + send(resultReceiver, requestCode, permissions, grantResults); + } + } + + public static void requestPermissions( + final Activity activity, final String[] permissions, final Callback callback) { + requestPermissions( + activity, + permissions, + new ResultReceiver(new Handler(Looper.getMainLooper())) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + callback.invoke( + resultData.getStringArray(PERMISSIONS), resultData.getIntArray(GRANT_RESULTS)); + } + }); + } + + private static void send( + ResultReceiver resultReceiver, int requestCode, String[] permissions, int[] grantResults) { + Bundle resultData = new Bundle(); + resultData.putStringArray(PERMISSIONS, permissions); + resultData.putIntArray(GRANT_RESULTS, grantResults); + + resultReceiver.send(requestCode, resultData); + } + + public interface Callback { + void invoke(String[] permissions, int[] grantResults); + } + + /** + * Helper activity for requesting permissions. Android only allows requesting permissions from an + * activity and the result is reported in the onRequestPermissionsResult method. Since + * this package is a library we create an auxiliary activity and communicate back the results + * using a ResultReceiver. + */ + @RequiresApi(api = VERSION_CODES.M) + public static class RequestPermissionsFragment extends Fragment { + private void checkSelfPermissions(boolean requestPermissions) { + // Figure out which of the requested permissions are actually denied + // because we do not want to ask about the granted permissions + // (which Android supports). + Bundle args = getArguments(); + String[] permissions = args.getStringArray(PERMISSIONS); + int size = permissions.length; + Activity activity = getActivity(); + int[] grantResults = new int[size]; + ArrayList deniedPermissions = new ArrayList<>(); + + for (int i = 0; i < size; ++i) { + String permission = permissions[i]; + int grantResult; + // No need to ask for permission on pre-Marshmallow + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + grantResult = PackageManager.PERMISSION_GRANTED; + else grantResult = activity.checkSelfPermission(permission); + + grantResults[i] = grantResult; + if (grantResult != PackageManager.PERMISSION_GRANTED) { + deniedPermissions.add(permission); } + } - Bundle args = new Bundle(); - args.putInt(REQUEST_CODE, requestCode); - args.putParcelable(RESULT_RECEIVER, resultReceiver); - args.putStringArray(PERMISSIONS, permissions); - - RequestPermissionsFragment fragment = new RequestPermissionsFragment(); - fragment.setArguments(args); - fragment.setPlugin(plugin); - - FragmentTransaction transaction - = activity.getFragmentManager().beginTransaction().add( - fragment, - fragment.getClass().getName() + "-" + requestCode); - - try { - transaction.commit(); - } catch (IllegalStateException ise) { - // Context is a Plugin, just send result back. - send(resultReceiver, requestCode, permissions, grantResults); - } - } + int requestCode = args.getInt(REQUEST_CODE, 0); - public static void requestPermissions( - final FlutterWebRTCPlugin plugin, - final String[] permissions, - final Callback callback) { + if (deniedPermissions.isEmpty() || !requestPermissions) { + // All permissions have already been granted or we cannot ask + // the user about the denied ones. + finish(); + send(args.getParcelable(RESULT_RECEIVER), requestCode, permissions, grantResults); + } else { + // Ask the user about the denied permissions. requestPermissions( - plugin, - permissions, - new ResultReceiver(new Handler(Looper.getMainLooper())) { - @Override - protected void onReceiveResult( - int resultCode, - Bundle resultData) { - callback.invoke( - resultData.getStringArray(PERMISSIONS), - resultData.getIntArray(GRANT_RESULTS)); - } - }); + deniedPermissions.toArray(new String[deniedPermissions.size()]), requestCode); + } } - private static void send( - ResultReceiver resultReceiver, - int requestCode, - String[] permissions, - int[] grantResults) { - Bundle resultData = new Bundle(); - resultData.putStringArray(PERMISSIONS, permissions); - resultData.putIntArray(GRANT_RESULTS, grantResults); + private void finish() { + Activity activity = getActivity(); - resultReceiver.send(requestCode, resultData); + if (activity != null) { + activity.getFragmentManager().beginTransaction().remove(this).commitAllowingStateLoss(); + } } - public interface Callback { - void invoke(String[] permissions, int[] grantResults); + @Override + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + Bundle args = getArguments(); + + if (args.getInt(REQUEST_CODE, 0) != requestCode) { + return; + } + + // XXX The super's documentation says: It is possible that the + // permissions request interaction with the user is interrupted. In + // this case you will receive empty permissions and results arrays + // which should be treated as a cancellation. + if (permissions.length == 0 || grantResults.length == 0) { + // The getUserMedia algorithm does not define a way to cancel + // the invocation so we have to redo the permission request. + finish(); + PermissionUtils.requestPermissions( + getActivity(), + args.getStringArray(PERMISSIONS), + (ResultReceiver) args.getParcelable(RESULT_RECEIVER)); + } else { + // We did not ask for all requested permissions, just the denied + // ones. But when we send the result, we have to answer about + // all requested permissions. + checkSelfPermissions(/* requestPermissions */ false); + } } - /** - * Helper activity for requesting permissions. Android only allows - * requesting permissions from an activity and the result is reported in the - * onRequestPermissionsResult method. Since this package is a - * library we create an auxiliary activity and communicate back the results - * using a ResultReceiver. - */ - public static class RequestPermissionsFragment extends Fragment { - private FlutterWebRTCPlugin plugin; - - public void setPlugin(FlutterWebRTCPlugin plugin){ - this.plugin = plugin; - } - private void checkSelfPermissions(boolean requestPermissions) { - // Figure out which of the requested permissions are actually denied - // because we do not want to ask about the granted permissions - // (which Android supports). - Bundle args = getArguments(); - String[] permissions = args.getStringArray(PERMISSIONS); - int size = permissions.length; - Activity activity = getActivity(); - int[] grantResults = new int[size]; - ArrayList deniedPermissions = new ArrayList<>(); - - for (int i = 0; i < size; ++i) { - String permission = permissions[i]; - int grantResult; - // No need to ask for permission on pre-Marshmallow - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) - grantResult = PackageManager.PERMISSION_GRANTED; - else - grantResult = activity.checkSelfPermission(permission); - - grantResults[i] = grantResult; - if (grantResult != PackageManager.PERMISSION_GRANTED) { - deniedPermissions.add(permission); - } - } - - int requestCode = args.getInt(REQUEST_CODE, 0); - - if (deniedPermissions.isEmpty() || !requestPermissions) { - // All permissions have already been granted or we cannot ask - // the user about the denied ones. - finish(); - send( - args.getParcelable(RESULT_RECEIVER), - requestCode, - permissions, - grantResults); - } else { - // Ask the user about the denied permissions. - requestPermissions( - deniedPermissions.toArray( - new String[deniedPermissions.size()]), - requestCode); - } - } - - private void finish() { - Activity activity = getActivity(); - - if (activity != null) { - activity.getFragmentManager().beginTransaction() - .remove(this) - .commitAllowingStateLoss(); - } - } + @Override + public void onResume() { + super.onResume(); - @Override - public void onRequestPermissionsResult( - int requestCode, - String[] permissions, - int[] grantResults) { - Bundle args = getArguments(); - - if (args.getInt(REQUEST_CODE, 0) != requestCode) { - return; - } - - // XXX The super's documentation says: It is possible that the - // permissions request interaction with the user is interrupted. In - // this case you will receive empty permissions and results arrays - // which should be treated as a cancellation. - if (permissions.length == 0 || grantResults.length == 0) { - // The getUserMedia algorithm does not define a way to cancel - // the invocation so we have to redo the permission request. - finish(); - PermissionUtils.requestPermissions( - plugin, - args.getStringArray(PERMISSIONS), - (ResultReceiver) args.getParcelable(RESULT_RECEIVER)); - } else { - // We did not ask for all requested permissions, just the denied - // ones. But when we send the result, we have to answer about - // all requested permissions. - checkSelfPermissions(/* requestPermissions */ false); - } - } - - @Override - public void onResume() { - super.onResume(); - - checkSelfPermissions(/* requestPermissions */ true); - } + checkSelfPermissions(/* requestPermissions */ true); } + } }