diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5daf793 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +## 0.9.6 [2016-02-11] + +### Release Notes + +This release has improved API for video elements, updated native WebRTC libs and some important bug fixes. + +### Features + +- New video element layout mechanism +- External authentication support +- Automatic selection of iOS APNS environment +- Updated WebRTC libs : iOS - m49, Andoid - m48 + +### Bugfixes + +- Fixed the call setup issue between devices (caused by incorrect buffering of ICE candidates) +- Fixed the whitelist plugin warning about security policy tag diff --git a/README.md b/README.md index 3b134d4..83b808e 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ var d = b6.startCall('usr:tom', {audio: true, video: true}); ``` ### Documentation -Bit6 Cordova Plugin exposes the same API as [Bit6 JS SDK](https://github.com/bit6/bit6-js-sdk). Check Bit6 [JS documentation](http://bit6.github.io/bit6-js-sdk/). +Bit6 Cordova Plugin exposes the same API as [Bit6 JS SDK](https://github.com/bit6/bit6-js-sdk). Check Bit6 [JS documentation](http://bit6.github.io/bit6-js-sdk/). ### Demo app The complete source code is available in the [demo repo](https://github.com/bit6/bit6-cordova-demo). Check out the same demo app running with JS SDK at http://demo.bit6.com. @@ -63,7 +63,7 @@ The complete source code is available in the [demo repo](https://github.com/bit6 ### Push notifications -Push Notification support is required for receiving incoming calls and messages. +Push Notification support is required for receiving incoming calls and messages. Bit6 depends on [PushNotification](https://github.com/Telerik-Verified-Plugins/PushNotification) plugin which will be installed automatically. @@ -75,5 +75,9 @@ Bit6 depends on [PushNotification](https://github.com/Telerik-Verified-Plugins/P 1. Get the project id and server key from [Google Dev Console](http://developer.android.com/google/gcm/gs.html). 2. Add project id and server key for your app in [Bit6 Dashboard](https://dashboard.bit6.com). +### Building with Xcode 7 +Please disable Bitcode support when building your Cordova app with Xcode. +Go to `Build Settings`, set `Enable Bitcode` to `No`. + ### Third-party libraries Bit6 plugin leverages code from the excellent [WebRTC](http://www.webrtc.org/), [PhoneRTC](https://github.com/alongubkin/phonertc) and [phonegap-websocket](https://github.com/mkuklis/phonegap-websocket/) projects. diff --git a/libs/android/armeabi-v7a/libjingle_peerconnection_so.so b/libs/android/armeabi-v7a/libjingle_peerconnection_so.so index 1d66840..0cd14ff 100755 Binary files a/libs/android/armeabi-v7a/libjingle_peerconnection_so.so and b/libs/android/armeabi-v7a/libjingle_peerconnection_so.so differ diff --git a/libs/android/libjingle_peerconnection.jar b/libs/android/libjingle_peerconnection.jar index 24125aa..012c123 100644 Binary files a/libs/android/libjingle_peerconnection.jar and b/libs/android/libjingle_peerconnection.jar differ diff --git a/libs/ios/libPhoneRTC.a b/libs/ios/libPhoneRTC.a index 1007a7f..8f56b23 100644 Binary files a/libs/ios/libPhoneRTC.a and b/libs/ios/libPhoneRTC.a differ diff --git a/plugin.xml b/plugin.xml index 602e03a..5213896 100644 --- a/plugin.xml +++ b/plugin.xml @@ -2,7 +2,7 @@ + version="0.9.6"> Bit6 Add voice and video calling, text and media messaging to your app @@ -13,7 +13,7 @@ - + @@ -22,10 +22,13 @@ - + - + + + + @@ -48,6 +51,7 @@ + diff --git a/src/android/com/dooble/phonertc/PhoneRTCPlugin.java b/src/android/com/dooble/phonertc/PhoneRTCPlugin.java index fecddaa..a5a766c 100644 --- a/src/android/com/dooble/phonertc/PhoneRTCPlugin.java +++ b/src/android/com/dooble/phonertc/PhoneRTCPlugin.java @@ -27,6 +27,7 @@ import org.webrtc.VideoRendererGui; import org.webrtc.VideoSource; import org.webrtc.VideoTrack; +import org.webrtc.RendererCommon; public class PhoneRTCPlugin extends CordovaPlugin { private AudioSource _audioSource; @@ -34,10 +35,10 @@ public class PhoneRTCPlugin extends CordovaPlugin { private VideoCapturer _videoCapturer; private VideoSource _videoSource; - + private PeerConnectionFactory _peerConnectionFactory; private Map _sessions; - + private VideoConfig _videoConfig; private VideoGLView _videoView; private List _remoteVideos; @@ -47,59 +48,58 @@ public class PhoneRTCPlugin extends CordovaPlugin { private boolean _initializedAndroidGlobals = false; private WebView _webView; - + public PhoneRTCPlugin() { _remoteVideos = new ArrayList(); _sessions = new HashMap(); } - + @Override public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { final CallbackContext _callbackContext = callbackContext; - - if (action.equals("createSessionObject")) { + + if (action.equals("createSessionObject")) { final SessionConfig config = SessionConfig.fromJSON(args.getJSONObject(1)); - + final String sessionKey = args.getString(0); _callbackContext.sendPluginResult(getSessionKeyPluginResult(sessionKey)); - + cordova.getActivity().runOnUiThread(new Runnable() { - public void run() { + public void run() { if (!_initializedAndroidGlobals) { - abortUnless(PeerConnectionFactory.initializeAndroidGlobals(cordova.getActivity(), true, true, - VideoRendererGui.getEGLContext()), + abortUnless(PeerConnectionFactory.initializeAndroidGlobals(cordova.getActivity(), true, true, true), "Failed to initializeAndroidGlobals"); _initializedAndroidGlobals = true; } - + if (_peerConnectionFactory == null) { _peerConnectionFactory = new PeerConnectionFactory(); } - + if (config.isAudioStreamEnabled() && _audioTrack == null) { initializeLocalAudioTrack(); } - - if (config.isVideoStreamEnabled() && _localVideo == null) { + + if (config.isVideoStreamEnabled() && _localVideo == null) { initializeLocalVideoTrack(); } - - _sessions.put(sessionKey, new Session(PhoneRTCPlugin.this, + + _sessions.put(sessionKey, new Session(PhoneRTCPlugin.this, _callbackContext, config, sessionKey)); - + if (_sessions.size() > 1) { _shouldDispose = false; } } }); - + return true; } else if (action.equals("call")) { JSONObject container = args.getJSONObject(0); final String sessionKey = container.getString("sessionKey"); - + cordova.getActivity().runOnUiThread(new Runnable() { public void run() { try { @@ -114,7 +114,7 @@ public void run() { } } }); - + return true; } else if (action.equals("receiveMessage")) { JSONObject container = args.getJSONObject(0); @@ -122,6 +122,7 @@ public void run() { final String message = container.getString("message"); Log.e("PRTC", "recvMsg: sess=" + sessionKey + " msg=" + message); + Log.e("PRTC", " - recvMsg to: sessObj=" + _sessions.get(sessionKey)); cordova.getThreadPool().execute(new Runnable() { public void run() { @@ -130,11 +131,11 @@ public void run() { }); return true; - } else if (action.equals("renegotiate")) { + } else if (action.equals("renegotiate")) { JSONObject container = args.getJSONObject(0); final String sessionKey = container.getString("sessionKey"); - final SessionConfig config = SessionConfig.fromJSON(container.getJSONObject("config")); - + final SessionConfig config = SessionConfig.fromJSON(container.getJSONObject("config")); + cordova.getActivity().runOnUiThread(new Runnable() { public void run() { Session session = _sessions.get(sessionKey); @@ -142,11 +143,11 @@ public void run() { session.createOrUpdateStream(); } }); - + } else if (action.equals("disconnect")) { JSONObject container = args.getJSONObject(0); final String sessionKey = container.getString("sessionKey"); - + cordova.getActivity().runOnUiThread(new Runnable() { @Override public void run() { @@ -155,25 +156,26 @@ public void run() { } } }); - + return true; } else if (action.equals("setVideoView")) { _videoConfig = VideoConfig.fromJSON(args.getJSONObject(0)); Log.e("PRTC", "setVideoView1"); + VideoConfig.VideoLayoutParams c = _videoConfig.getContainer(); + Log.e("PRTC", "setVideoView1 size: " + c.getWidth() + "x" + c.getHeight()); // make sure it's not junk if (_videoConfig.getContainer().getWidth() == 0 || _videoConfig.getContainer().getHeight() == 0) { return false; } - + cordova.getActivity().runOnUiThread(new Runnable() { public void run() { if (!_initializedAndroidGlobals) { - abortUnless(PeerConnectionFactory.initializeAndroidGlobals(cordova.getActivity(), true, true, - VideoRendererGui.getEGLContext()), + abortUnless(PeerConnectionFactory.initializeAndroidGlobals(cordova.getActivity(), true, true, true), "Failed to initializeAndroidGlobals"); _initializedAndroidGlobals = true; } - + if (_peerConnectionFactory == null) { _peerConnectionFactory = new PeerConnectionFactory(); } @@ -188,15 +190,15 @@ public void run() { y = _videoConfig.getDeviceValue(y); _videoParams = new WebView.LayoutParams(w, h, x, y); - + if (_videoView == null) { // createVideoView(); - + if (_videoConfig.getLocal() != null && _localVideo == null) { Log.e("PRTC", "initLocalVideo"); initializeLocalVideoTrack(); } - } else { + } else { _videoView.setLayoutParams(_videoParams); // AG: Show automatically upon the change of coords. // Otherwise it's weird that the first call shows it @@ -205,7 +207,7 @@ public void run() { } } }); - + return true; } else if (action.equals("hideVideoView")) { cordova.getActivity().runOnUiThread(new Runnable() { @@ -218,7 +220,7 @@ public void run() { public void run() { _videoView.setVisibility(View.VISIBLE); } - }); + }); } callbackContext.error("Invalid action: " + action); @@ -227,38 +229,38 @@ public void run() { void initializeLocalVideoTrack() { _videoCapturer = getVideoCapturer(); - _videoSource = _peerConnectionFactory.createVideoSource(_videoCapturer, + _videoSource = _peerConnectionFactory.createVideoSource(_videoCapturer, new MediaConstraints()); _localVideo = new VideoTrackRendererPair(_peerConnectionFactory.createVideoTrack("ARDAMSv0", _videoSource), null); Log.e("PRTC", "Got local source " + _videoSource + " and video" + _localVideo); - refreshVideoView(); + refreshVideoView(); } - + int getPercentage(int localValue, int containerValue) { return (int)(localValue * 100.0 / containerValue); } - + void initializeLocalAudioTrack() { _audioSource = _peerConnectionFactory.createAudioSource(new MediaConstraints()); _audioTrack = _peerConnectionFactory.createAudioTrack("ARDAMSa0", _audioSource); } - + public VideoTrack getLocalVideoTrack() { if (_localVideo == null) { return null; } - + return _localVideo.getVideoTrack(); } - + public AudioTrack getLocalAudioTrack() { return _audioTrack; } - + public PeerConnectionFactory getPeerConnectionFactory() { return _peerConnectionFactory; } - + public Activity getActivity() { return cordova.getActivity(); } @@ -296,11 +298,11 @@ public WebView getWebView() { } return _webView; } - + public VideoConfig getVideoConfig() { return this._videoConfig; } - + private static void abortUnless(boolean condition, String msg) { if (!condition) { throw new RuntimeException(msg); @@ -337,22 +339,22 @@ public void addRemoteVideoTrack(VideoTrack videoTrack) { } public void removeRemoteVideoTrack(VideoTrack videoTrack) { - for (VideoTrackRendererPair pair : _remoteVideos) { + for (VideoTrackRendererPair pair : _remoteVideos) { if (pair.getVideoTrack() == videoTrack) { if (pair.getVideoRenderer() != null) { pair.getVideoTrack().removeRenderer(pair.getVideoRenderer()); pair.setVideoRenderer(null); } - + pair.setVideoTrack(null); - + _remoteVideos.remove(pair); refreshVideoView(); return; } } } - + private void createVideoView() { Point size = new Point(); int w = _videoConfig.getContainer().getWidth(); @@ -362,27 +364,32 @@ private void createVideoView() { Log.e("PRTC", "videoViewSize: " + size.x + "x" + size.y); _videoView = new VideoGLView(cordova.getActivity(), size); - VideoRendererGui.setView(_videoView); - + VideoRendererGui.setView(_videoView, new Runnable() { + @Override + public void run() { + Log.e("PhoneRTCPlugin", "setView finished"); + } + }); + getWebView().addView(_videoView, _videoParams); } - + private void refreshVideoView() { int n = _remoteVideos.size(); - + for (VideoTrackRendererPair pair : _remoteVideos) { if (pair.getVideoRenderer() != null) { pair.getVideoTrack().removeRenderer(pair.getVideoRenderer()); } - + pair.setVideoRenderer(null); } - + if (_localVideo != null && _localVideo.getVideoRenderer() != null) { _localVideo.getVideoTrack().removeRenderer(_localVideo.getVideoRenderer()); _localVideo.setVideoRenderer(null); } - + if (_videoView != null) { getWebView().removeView(_videoView); _videoView = null; @@ -390,8 +397,8 @@ private void refreshVideoView() { // AG: Create VideoView in any case, even if no remote streams createVideoView(); - - if (n > 0) { + + if (n > 0) { int videosInRow = n; int rows = 1; if (n > 4) { @@ -407,51 +414,51 @@ else if (n > 2) { int vh = 100 / rows; int videoIndex = 0; - for (int row = 0, y = 0; row < rows && videoIndex < n; row++, y+=vh) { + for (int row = 0, y = 0; row < rows && videoIndex < n; row++, y+=vh) { for (int col = 0, x = 0; col < videosInRow && videoIndex < n; col++, x+=vw) { VideoTrackRendererPair pair = _remoteVideos.get(videoIndex++); Log.e("PRTC", "remoteVideo: x=" + x + " y=" + y + " w=" + vw + " h=" + vh); pair.setVideoRenderer(new VideoRenderer( VideoRendererGui.create( - x, y, vw, vh, - VideoRendererGui.ScalingType.SCALE_FILL, true + x, y, vw, vh, + RendererCommon.ScalingType.SCALE_ASPECT_FILL, true ) )); - - pair.getVideoTrack().addRenderer(pair.getVideoRenderer()); + + pair.getVideoTrack().addRenderer(pair.getVideoRenderer()); } } /* int rows = n < 9 ? 2 : 3; int videosInRow = n == 2 ? 2 : (int)Math.ceil((float)n / rows); - + int videoSize = (int)((float)_videoConfig.getContainer().getWidth() / videosInRow); int actualRows = (int)Math.ceil((float)n / videosInRow); - + int y = getCenter(actualRows, videoSize, _videoConfig.getContainer().getHeight()); - + int videoIndex = 0; int videoSizeAsPercentage = getPercentage(videoSize, _videoConfig.getContainer().getWidth()); - + for (int row = 0; row < rows && videoIndex < n; row++) { - int x = getCenter(row < row - 1 || n % rows == 0 ? + int x = getCenter(row < row - 1 || n % rows == 0 ? videosInRow : n - (Math.min(n, videoIndex + videosInRow) - 1), videoSize, _videoConfig.getContainer().getWidth()); - + for (int video = 0; video < videosInRow && videoIndex < n; video++) { VideoTrackRendererPair pair = _remoteVideos.get(videoIndex++); Log.e("PRTC", "remoteVideo: x=" + x + " y=" + y + " sz=" + videoSizeAsPercentage); pair.setVideoRenderer(new VideoRenderer( - VideoRendererGui.create(x, y, videoSizeAsPercentage, videoSizeAsPercentage, + VideoRendererGui.create(x, y, videoSizeAsPercentage, videoSizeAsPercentage, VideoRendererGui.ScalingType.SCALE_FILL, true))); - + pair.getVideoTrack().addRenderer(pair.getVideoRenderer()); - + x += videoSizeAsPercentage; } - + y += getPercentage(videoSize, _videoConfig.getContainer().getHeight()); } */ @@ -462,36 +469,36 @@ else if (n > 2) { VideoConfig.VideoLayoutParams p = _videoConfig.getLocal(); _localVideo.getVideoTrack().addRenderer(new VideoRenderer( VideoRendererGui.create( - _videoConfig.getWidthPercentage(p.getX()), - _videoConfig.getHeightPercentage(p.getY()), - _videoConfig.getWidthPercentage(p.getWidth()), + _videoConfig.getWidthPercentage(p.getX()), + _videoConfig.getHeightPercentage(p.getY()), + _videoConfig.getWidthPercentage(p.getWidth()), _videoConfig.getHeightPercentage(p.getHeight()), - VideoRendererGui.ScalingType.SCALE_FILL, + RendererCommon.ScalingType.SCALE_ASPECT_FILL, true ) - )); + )); } } int getCenter(int videoCount, int videoSize, int containerSize) { return getPercentage((int)Math.round((containerSize - videoSize * videoCount) / 2.0), containerSize); } - + PluginResult getSessionKeyPluginResult(String sessionKey) throws JSONException { JSONObject json = new JSONObject(); json.put("type", "__set_session_key"); json.put("sessionKey", sessionKey); - + PluginResult result = new PluginResult(PluginResult.Status.OK, json); result.setKeepCallback(true); - + return result; } - + public void onSessionDisconnect(String sessionKey) { _sessions.remove(sessionKey); - + if (_sessions.size() == 0) { cordova.getActivity().runOnUiThread(new Runnable() { public void run() { @@ -499,10 +506,10 @@ public void run() { if (_localVideo.getVideoTrack() != null && _localVideo.getVideoRenderer() != null) { _localVideo.getVideoTrack().removeRenderer(_localVideo.getVideoRenderer()); } - - _localVideo = null; + + _localVideo = null; } - + if (_videoView != null) { _videoView.setVisibility(View.GONE); getWebView().removeView(_videoView); @@ -514,36 +521,36 @@ public void run() { } else { _videoSource.stop(); } - + _videoSource = null; } - + if (_videoCapturer != null) { _videoCapturer.dispose(); _videoCapturer = null; } - + if (_audioSource != null) { _audioSource.dispose(); _audioSource = null; - + _audioTrack = null; } - + _videoConfig = null; - + // if (_peerConnectionFactory != null) { // _peerConnectionFactory.dispose(); // _peerConnectionFactory = null; // } - + _remoteVideos.clear(); _shouldDispose = true; } }); } - } - + } + public boolean shouldDispose() { return _shouldDispose; } diff --git a/src/android/com/dooble/phonertc/Session.java b/src/android/com/dooble/phonertc/Session.java index ad69568..1494162 100644 --- a/src/android/com/dooble/phonertc/Session.java +++ b/src/android/com/dooble/phonertc/Session.java @@ -26,29 +26,29 @@ public class Session { CallbackContext _callbackContext; SessionConfig _config; String _sessionKey; - + MediaConstraints _sdpMediaConstraints; PeerConnection _peerConnection; - + private LinkedList _queuedRemoteCandidates; private Object _queuedRemoteCandidatesLocker = new Object(); - + private MediaStream _localStream; private VideoTrack _videoTrack; - + // Synchronize on quit[0] to avoid teardown-related crashes. private final Boolean[] _quit = new Boolean[] { false }; - + private final SDPObserver _sdpObserver = new SDPObserver(); private final PCObserver _pcObserver = new PCObserver(); - + public Session(PhoneRTCPlugin plugin, CallbackContext callbackContext, SessionConfig config, String sessionKey) { _plugin = plugin; _callbackContext = callbackContext; _config = config; _sessionKey = sessionKey; } - + public void call() { _queuedRemoteCandidates = new LinkedList(); _quit[0] = false; @@ -57,42 +57,49 @@ public void call() { final LinkedList iceServers = new LinkedList(); iceServers.add(new PeerConnection.IceServer("stun:stun.l.google.com:19302")); iceServers.add(new PeerConnection.IceServer(_config.getTurnServerHost(), - _config.getTurnServerUsername(), + _config.getTurnServerUsername(), _config.getTurnServerPassword())); - + // Initialize SDP media constraints _sdpMediaConstraints = new MediaConstraints(); _sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair( "OfferToReceiveAudio", "true")); - _sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair( - "OfferToReceiveVideo", _plugin.getVideoConfig() == null ? "false" : "true")); - + // Since OfferToReceiveVideo depends on Plugin having the VideoView + // we have to check it right before creating local SDP + //_sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair( + // "OfferToReceiveVideo", _plugin.getVideoConfig() == null ? "false" : "true")); + // Initialize PeerConnection MediaConstraints pcMediaConstraints = new MediaConstraints(); pcMediaConstraints.optional.add(new MediaConstraints.KeyValuePair( "DtlsSrtpKeyAgreement", "true")); - + _peerConnection = _plugin.getPeerConnectionFactory() .createPeerConnection(iceServers, pcMediaConstraints, _pcObserver); - + // Initialize local stream createOrUpdateStream(); // Create offer if initiator if (_config.isInitiator()) { + // Since OfferToReceiveVideo depends on Plugin having the VideoView + // we have to check it right before creating local SDP + _sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair( + "OfferToReceiveVideo", _plugin.getVideoConfig() == null ? "false" : "true")); _peerConnection.createOffer(_sdpObserver, _sdpMediaConstraints); } } - + public void receiveMessage(String message) { + Log.e("com.dooble.phonertc", "Sess.recvMsg: " + message); try { JSONObject json = new JSONObject(message); - String type = (String) json.get("type"); + final String type = (String) json.get("type"); if (type.equals("candidate")) { final IceCandidate candidate = new IceCandidate( (String) json.get("id"), json.getInt("label"), (String) json.get("candidate")); - + synchronized (_queuedRemoteCandidatesLocker) { if (_queuedRemoteCandidates != null) { _queuedRemoteCandidates.add(candidate); @@ -103,7 +110,7 @@ public void run() { _peerConnection.addIceCandidate(candidate); } } - }); + }); } } @@ -113,6 +120,8 @@ public void run() { preferISAC((String) json.get("sdp"))); _plugin.getActivity().runOnUiThread(new Runnable() { public void run() { + //Log.e("com.dooble.phonertc", "SDP.setRemote type=" + type); + //Log.e("com.dooble.phonertc", "SDP.setRemote obj=" + sdp.description); _peerConnection.setRemoteDescription(_sdpObserver, sdp); } }); @@ -131,26 +140,26 @@ public void run() { throw new RuntimeException(e); } } - + public void createOrUpdateStream() { if (_localStream != null) { _peerConnection.removeStream(_localStream); _localStream = null; } - + _localStream = _plugin.getPeerConnectionFactory().createLocalMediaStream("ARDAMS"); - + if (_config.isAudioStreamEnabled() && _plugin.getLocalAudioTrack() != null) { _localStream.addTrack(_plugin.getLocalAudioTrack()); } - + if (_config.isVideoStreamEnabled() && _plugin.getLocalVideoTrack() != null) { _localStream.addTrack(_plugin.getLocalVideoTrack()); } - + _peerConnection.addStream(_localStream); } - + void sendMessage(JSONObject data) { PluginResult result = new PluginResult(PluginResult.Status.OK, data); result.setKeepCallback(true); @@ -214,14 +223,14 @@ public void disconnect(boolean sendByeMessage) { if (_quit[0]) { return; } - + _quit[0] = true; - + if (_videoTrack != null) { _plugin.removeRemoteVideoTrack(_videoTrack); _videoTrack = null; } - + if (sendByeMessage) { try { JSONObject data = new JSONObject(); @@ -229,27 +238,27 @@ public void disconnect(boolean sendByeMessage) { sendMessage(data); } catch (JSONException e) {} } - + if (_peerConnection != null) { if (_plugin.shouldDispose()) { _peerConnection.dispose(); } else { _peerConnection.close(); } - + _peerConnection = null; } - + try { JSONObject data = new JSONObject(); data.put("type", "__disconnected"); sendMessage(data); - } catch (JSONException e) {} - + } catch (JSONException e) {} + _plugin.onSessionDisconnect(_sessionKey); } } - + public void setConfig(SessionConfig config) { _config = config; } @@ -281,12 +290,12 @@ public void onAddStream(final MediaStream stream) { public void run() { if (stream.videoTracks.size() > 0) { _videoTrack = stream.videoTracks.get(0); - + if (_videoTrack != null) { _plugin.addRemoteVideoTrack(_videoTrack); } } - + try { JSONObject data = new JSONObject(); data.put("type", "__answered"); @@ -341,6 +350,11 @@ public void onSignalingChange( } + @Override + public void onIceConnectionReceivingChange(boolean b) { + // TODO Auto-generated method stub + } + } private class SDPObserver implements SdpObserver { @@ -348,12 +362,15 @@ private class SDPObserver implements SdpObserver { public void onCreateSuccess(final SessionDescription origSdp) { _plugin.getActivity().runOnUiThread(new Runnable() { public void run() { + //Log.e("com.dooble.phonertc", "SDP.onCreate" + origSdp.type); SessionDescription sdp = new SessionDescription( origSdp.type, preferISAC(origSdp.description)); try { JSONObject json = new JSONObject(); json.put("type", sdp.type.canonicalForm()); json.put("sdp", sdp.description); + //Log.e("com.dooble.phonertc", "SDP.setLocal type=" + sdp.type); + //Log.e("com.dooble.phonertc", "SDP.setLocal obj=" + sdp.description); sendMessage(json); _peerConnection.setLocalDescription(_sdpObserver, sdp); } catch (JSONException e) { @@ -379,6 +396,10 @@ public void run() { if (_peerConnection.getLocalDescription() == null) { // We just set the remote offer, time to create our // answer. + // Since OfferToReceiveVideo depends on Plugin having the VideoView + // we have to check it right before creating local SDP + _sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair( + "OfferToReceiveVideo", _plugin.getVideoConfig() == null ? "false" : "true")); _peerConnection.createAnswer(SDPObserver.this, _sdpMediaConstraints); } else { @@ -396,16 +417,18 @@ public void run() { public void onCreateFailure(final String error) { _plugin.getActivity().runOnUiThread(new Runnable() { public void run() { + Log.e("com.dooble.phonertc", "SDP.onCreateErr" + error); throw new RuntimeException("createSDP error: " + error); } }); } - + @Override public void onSetFailure(final String error) { _plugin.getActivity().runOnUiThread(new Runnable() { public void run() { - //throw new RuntimeException("setSDP error: " + error); + Log.e("com.dooble.phonertc", "SDP.onSetErr: " + error); + throw new RuntimeException("setSDP error: " + error); } }); } @@ -414,13 +437,13 @@ private void drainRemoteCandidates() { synchronized (_queuedRemoteCandidatesLocker) { if (_queuedRemoteCandidates == null) return; - + for (IceCandidate candidate : _queuedRemoteCandidates) { _peerConnection.addIceCandidate(candidate); } - + _queuedRemoteCandidates = null; } } } -} \ No newline at end of file +} diff --git a/src/ios/PhoneRTCPlugin.h b/src/ios/PhoneRTCPlugin.h index b805ade..831f830 100644 --- a/src/ios/PhoneRTCPlugin.h +++ b/src/ios/PhoneRTCPlugin.h @@ -50,6 +50,9 @@ -(void) addRemoteVideoTrack: (RTCVideoTrack*)videoTrack; -(void) removeRemoteVideoTrack: (RTCVideoTrack*)videoTrack; +//Not a PhoneRTC specific thing. Used for iOS push notifications. +- (void) isApnsProduction: (CDVInvokedUrlCommand*)command; + @end diff --git a/src/ios/PhoneRTCPlugin.m b/src/ios/PhoneRTCPlugin.m index 39ed54b..8d1fa44 100644 --- a/src/ios/PhoneRTCPlugin.m +++ b/src/ios/PhoneRTCPlugin.m @@ -351,6 +351,17 @@ -(void) onSessionDisconnect:(NSString*) sessionKey } } +-(void) isApnsProduction: (CDVInvokedUrlCommand*)command +{ + BOOL apnsProduction = false; + if (![[NSBundle mainBundle] pathForResource:@"embedded" ofType:@"mobileprovision"]) { +  apnsProduction = true; + } + + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsBool:apnsProduction]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; +} + @end @implementation VideoTrackViewPair diff --git a/www/bit6.js b/www/bit6.js index 2af5619..8527ebf 100644 --- a/www/bit6.js +++ b/www/bit6.js @@ -89,6 +89,10 @@ this.uri = this.id; } + Conversation.prototype.isGroup = function() { + return this.id.indexOf('grp:') === 0; + }; + Conversation.prototype.getUnreadCount = function() { return this.unread; }; @@ -159,14 +163,14 @@ console.log('ConvId is null', this); return null; } - return this.id.replace(':', '--').replace('.', '_-').replace('+', '-_'); + return this.id.replace(/\:/g, '--').replace(/\./g, '_-').replace(/\+/g, '-_'); }; Conversation.fromDomId = function(t) { if (!t) { return t; } - return t.replace('--', ':').replace('_-', '.').replace('-_', '+'); + return t.replace(/--/g, ':').replace(/_-/g, '.').replace(/-_/g, '+'); }; return Conversation; @@ -183,20 +187,37 @@ extend(Dialog, superClass); function Dialog(client, outgoing, other, options) { - var myaddr; + var base, i, j, len, len1, myaddr, ref, ref1, t; this.client = client; this.outgoing = outgoing; this.other = other; this.options = options; Dialog.__super__.constructor.apply(this, arguments); this.me = this.client.session.identity; + if (this.options == null) { + this.options = {}; + } + ref = ['audio', 'video', 'screen', 'data']; + for (i = 0, len = ref.length; i < len; i++) { + t = ref[i]; + if ((base = this.options)[t] == null) { + base[t] = false; + } + } + if (this.other.indexOf('pstn:') !== 0 && this.other.indexOf('grp:') !== 0) { + this.options.data = true; + } + this.remoteOptions = {}; + ref1 = ['audio', 'video']; + for (j = 0, len1 = ref1.length; j < len1; j++) { + t = ref1[j]; + this.remoteOptions[t] = this.options[t] && !this.outgoing; + } this.params = { - useVideo: this.options.video, - useStereo: false, - tag: 'webcam', callID: null }; myaddr = 'uid:' + this.client.session.userid + '@' + this.client.apikey; + this.renegotiating = false; if (this.outgoing) { this.state = 'req'; this.params.destination_number = this.other + '@' + this.client.apikey; @@ -212,48 +233,62 @@ } Dialog.prototype.connect = function(opts) { - var i, k, len, ref, ref1; + var i, len, newv, oldv, ref, t; if (opts == null) { opts = {}; } - if (!this.options.audio && !this.options.video) { - this._onMediaReady(); - return true; - } - if (this.options.video) { - if (!(opts.containerEl || (opts.localMediaEl && opts.remoteMediaEl))) { - this.emit('error', 'Need container or specific video elements'); - return this.hangup(); + ref = ['audio', 'video', 'screen']; + for (i = 0, len = ref.length; i < len; i++) { + t = ref[i]; + if (opts[t] == null) { + continue; } + newv = opts[t]; + oldv = this.options[t]; + if (oldv === newv) { + continue; + } + this.options[t] = newv; } - ref = ['containerEl', 'localMediaEl', 'remoteMediaEl']; - for (i = 0, len = ref.length; i < len; i++) { - k = ref[i]; - this.options[k] = (ref1 = opts[k]) != null ? ref1 : null; - } - console.log('Dialog - calling ensure media', this); - this.client._ensureRtcMedia(this.options, 1, (function(_this) { - return function(ok) { - console.log('Dialog - media available: ok=', ok, 'd=', _this); - if (!ok) { + this.client._ensureRtcCapture(this.options, (function(_this) { + return function(err) { + if (err != null) { _this.emit('error', 'Unable to start media'); return _this.hangup(); } - _this._onMediaReady(); - if (_this.options.video) { - return _this.emit('videos'); - } + return _this._onMediaReady(); }; })(this)); - return true; + return this; + }; + + Dialog.prototype.hasVideoStreams = function() { + return this.options.video || this.remoteOptions.video; }; Dialog.prototype._onMediaReady = function() { - var iceServers; - if (!this.outgoing) { - this._sendAcceptRejectIncomingCall(true); + var msg; + if (!this.renegotiating) { + if (!this.outgoing) { + this._sendAcceptRejectIncomingCall(true); + } + this.emit('progress'); } - this.emit('progress'); + if (this.rtc == null) { + this._createRtc(); + } + this.rtc.update(this.client.capture, this.options, this.remoteOptions); + if (this.renegotiating && !this.outgoing) { + msg = { + audio: this.options.audio, + video: this.options.video + }; + return this.sendJson('reneg2', msg); + } + }; + + Dialog.prototype._createRtc = function() { + var iceServers; this.rtc = this.client._createRtc(); this.rtc.on('offerAnswer', (function(_this) { return function(offerAnswer) { @@ -264,9 +299,9 @@ }); }; })(this)); - this.rtc.on('videos', (function(_this) { - return function() { - return _this.emit('videos'); + this.rtc.on('video', (function(_this) { + return function(v, op) { + return _this.client._emitVideoEvent(v, _this, op); }; })(this)); this.rtc.on('dcOpen', (function(_this) { @@ -276,7 +311,29 @@ })(this)); this.rtc.on('transfer', (function(_this) { return function(tr) { - _this.emit('transfer', tr); + var n, o; + if (!tr.outgoing) { + n = tr.info.name; + } + if ('offer2' === n || 'answer2' === n) { + if (tr.completed()) { + o = tr.json(); + _this._gotRemoteOfferAnswer(o.type, o); + } + } else if ('reneg2' === n) { + if (tr.completed()) { + o = tr.json(); + if (o.audio != null) { + _this.remoteOptions.audio = o.audio; + } + if (o.video != null) { + _this.remoteOptions.video = o.video; + } + _this.connect(); + } + } else { + _this.emit('transfer', tr); + } if (tr.err) { } else if (tr.completed() && tr.outgoing) { @@ -285,8 +342,7 @@ }; })(this)); iceServers = this.client.session.config.rtc.iceServers; - this.rtc.init(this.client.media, this.outgoing, iceServers, this.options); - return this.rtc.start(); + return this.rtc.init(this.outgoing, iceServers); }; Dialog.prototype._startNextPendingTransfer = function() { @@ -321,15 +377,23 @@ })(this)); }; + Dialog.prototype.sendJson = function(name, o) { + var tr; + tr = new bit6.Transfer(true, { + name: name + }); + tr.json(o); + this.transfers.push(tr); + if (tr.data != null) { + return this.rtc.startOutgoingTransfer(tr); + } + }; + Dialog.prototype.hangup = function() { if (this.rtc) { this.rtc.stop(); this.rtc = null; - this.client._ensureRtcMedia(null, -1); this._sendHangupCall(); - if (this.options.video) { - this.emit('videos'); - } } else if (!this.outgoing) { this._sendAcceptRejectIncomingCall(false); } @@ -340,14 +404,22 @@ var msg, ref, ref1; msg = offerAnswer; if (msg.type === 'offer') { - this.state = 'sent-offer'; - msg.dialogParams = this.params; - return (ref = this.client.rpc) != null ? ref.call('verto.invite', msg, cb) : void 0; + if (this.renegotiating) { + return this.sendJson('offer2', msg); + } else { + this.state = 'sent-offer'; + msg.dialogParams = this.params; + return (ref = this.client.rpc) != null ? ref.call('verto.invite', msg, cb) : void 0; + } } else if (msg.type === 'answer') { - this.state = 'sent-answer'; - this.params.wantVideo = this.options.video; - msg.dialogParams = this.params; - return (ref1 = this.client.rpc) != null ? ref1.call('verto.answer', msg, cb) : void 0; + if (this.renegotiating) { + return this.sendJson('answer2', msg); + } else { + this.state = 'sent-answer'; + msg.dialogParams = this.params; + this.renegotiating = true; + return (ref1 = this.client.rpc) != null ? ref1.call('verto.answer', msg, cb) : void 0; + } } }; @@ -386,8 +458,11 @@ Dialog.prototype._gotRemoteOfferAnswer = function(type, offerAnswer) { this.state = 'got-' + type; offerAnswer.type = type; + if (type === 'answer') { + this.renegotiating = true; + } if (this.rtc != null) { - return this.rtc.gotRemoteOfferAnswer(offerAnswer); + return this.rtc.gotRemoteOfferAnswer(offerAnswer, this.client.capture); } else { return console.log('Error: RTC not inited'); } @@ -398,10 +473,6 @@ if (this.rtc != null) { this.rtc.stop(); this.rtc = null; - this.client._ensureRtcMedia(null, -1); - if (this.options.video) { - this.emit('videos'); - } return this.emit('end'); } }; @@ -422,18 +493,14 @@ this.updated = 0; } - Group.prototype.update = function(o) { + Group.prototype.update = function(o, forceUpdate) { var k, v; - if (o.updated == null) { - return false; - } - if (this.updated === o.updated) { - if (JSON.stringify(this.meta) === JSON.stringify(o != null ? o.meta : void 0)) { - if (JSON.stringify(this.permissions) === JSON.stringify(o != null ? o.permissions : void 0)) { - if (JSON.stringify(this.members) === JSON.stringify(o != null ? o.members : void 0)) { - return false; - } - } + if (!forceUpdate) { + if (o.updated == null) { + return false; + } + if (this.updated === o.updated) { + return false; } } for (k in o) { @@ -456,6 +523,22 @@ return false; }; + Group.prototype.getMember = function(ident) { + var i, len, m, ref; + ref = this.members; + for (i = 0, len = ref.length; i < len; i++) { + m = ref[i]; + if (m.id === ident) { + return m; + } + } + return null; + }; + + Group.prototype.getConversationId = function() { + return 'grp:' + this.id; + }; + return Group; })(); @@ -496,7 +579,9 @@ return _this.queue = []; } else { return _this.call('login', {}, function(err, result) { - return console.log('rpc login err=', err, 'result=', result); + if (err) { + return console.log('rpc login err=', err, 'result=', result); + } }); } }; @@ -535,8 +620,8 @@ } } catch (error) { ex = error; - console.log('Exception parsing JSON response ', ex); - return console.log(' -- RAW {{{', e.data, '}}}'); + console.log('Exception parsing JSON response ' + ex); + return console.log(' -- RAW {{{' + e.data + '}}}'); } }; })(this); @@ -663,7 +748,7 @@ this.groups = {}; this.presence = {}; this.lastTypingSent = 0; - this.media = null; + this.capture = null; return this.dialogs = []; }; @@ -671,13 +756,6 @@ this._connectRt(); return this._loadMe((function(_this) { return function(err) { - var g, id, ref; - ref = _this.groups; - for (id in ref) { - g = ref[id]; - g.updated = 0; - _this._loadGroupWithMembers(id); - } return cb(null); }; })(this)); @@ -703,7 +781,6 @@ if (err) { return cb(err); } - console.log('LoadMe got', result, headers); _this.lastSince = (ref = headers != null ? headers.etag : void 0) != null ? ref : 0; ref1 = ['devices', 'identities', 'data', 'profile']; for (i = 0, len = ref1.length; i < len; i++) { @@ -713,7 +790,7 @@ } } if (result.groups != null) { - _this._processGroupInfos(result.groups); + _this._processGroupMemberships(result.groups); } _this._processMessages(result.messages); return cb(); @@ -787,7 +864,7 @@ other = encodeURIComponent(conv.id); return this.api('/me/messages?other=' + other, 'DELETE', (function(_this) { return function(err, result) { - var i, len, m, msgs; + var i, len, m, msgs, op; if (err) { return typeof cb === "function" ? cb(err) : void 0; } @@ -797,9 +874,13 @@ m.deleted = Date.now(); _this._processMessage(m, true); } - delete _this.conversations[conv.id]; - conv.deleted = Date.now(); - _this.emit('conversation', conv, -1); + op = 0; + if (!conv.isGroup()) { + op = -1; + delete _this.conversations[conv.id]; + conv.deleted = Date.now(); + } + _this.emit('conversation', conv, op); return typeof cb === "function" ? cb(null) : void 0; }; })(this)); @@ -818,7 +899,6 @@ continue; } m.status(bit6.Message.READ); - console.log('Msg to be marked: ', m); this._processMessage(m); num++; } @@ -829,7 +909,9 @@ this.api('/me/messages?other=' + other, 'PUT', { status: 'read' }, function(err, result) { - return console.log('markAsRead result=', result); + if (err) { + return console.log('markAsRead result=', result, 'err=', err); + } }); return num; }; @@ -858,7 +940,6 @@ _this._failOutgoingMessage(m); return typeof cb === "function" ? cb(err) : void 0; } else { - console.log('Msg after POST', o); _this._processMessage(o); tmp = { id: tmpId, @@ -876,13 +957,10 @@ return false; } m.status(bit6.Message.READ); - console.log('Msg to be marked: ', m); this._processMessage(m); this.api('/me/messages/' + m.id, 'PUT', { status: 'read' - }, function(err, result) { - return console.log('markAsRead result=', result); - }); + }, function(err, result) {}); return true; }; @@ -995,87 +1073,81 @@ if (err) { return cb(err); } - return _this._loadGroupWithMembers(o.id, function(err) { - if (err) { - return cb(err); - } - return cb(null, _this.getGroup(o.id)); - }); + return cb(null, _this.getGroup(o.id)); }); }; })(this)); }; - Client.prototype.joinGroup = function(id, role, cb) { - var g, memberInfo, ref; - g = this.getGroup(id); - if ((g != null ? (ref = g.me) != null ? ref.role : void 0 : void 0) === role) { - return cb(null, g); + Client.prototype.joinGroup = function(g, role, cb) { + return this.inviteGroupMember(g, 'me', role, cb); + }; + + Client.prototype.leaveGroup = function(g, cb) { + return this.kickGroupMember(g, 'me', cb); + }; + + Client.prototype.inviteGroupMember = function(g, ident, role, cb) { + var gid, memberInfo; + gid = g.id != null ? g.id : g; + if (ident === 'me') { + ident = this.session.identity; } memberInfo = { - id: this.session.identity, + id: ident, role: role }; - return this.api('/groups/' + id + '/members', 'POST', memberInfo, (function(_this) { + return this.api('/groups/' + g.id + '/members', 'POST', memberInfo, (function(_this) { return function(err, member) { if (err) { return cb(err); } - console.log('Became group member grpId', id); return _this._loadMe(function(err) { if (err) { return cb(err); } - return _this._loadGroupWithMembers(id, function(err) { - if (err) { - return cb(err); - } - return cb(null, _this.getGroup(id)); - }); + return cb(null); }); }; })(this)); }; - Client.prototype.leaveGroup = function(id, cb) { - var g; - g = this.getGroup(id); + Client.prototype.kickGroupMember = function(g, m, cb) { + if (g.id == null) { + g = this.getGroup(g); + } if (g == null) { return cb(null); } - delete this.groups[id]; - this.emit('group', g, -1); - return this.api('/groups/' + id + '/members/me', 'DELETE', (function(_this) { - return function(err, member) { + if (m === 'me') { + m = g.me.identity; + } + if (!m.id) { + m = g.getMember(m); + } + if (m == null) { + return cb(null); + } + return this.api('/groups/' + g.id + '/members/' + m.id, 'DELETE', (function(_this) { + return function(err) { if (err) { return cb(err); } - console.log('Left group', id); - return cb(null); - }; - })(this)); - }; - - Client.prototype._loadGroupWithMembers = function(id, cb) { - return this.api('/groups/' + id, { - embed: 'members' - }, (function(_this) { - return function(err, result) { - console.log('Loaded group', id, 'with members: ', result, err); - if (result) { - _this._processGroupDeltas(result); - } - if (cb != null) { - return cb(err, result); - } + return _this._loadMe(function(err) { + if (err) { + return cb(err); + } + return cb(null); + }); }; })(this)); }; - Client.prototype._processGroupInfos = function(infos) { - var g, i, id, info, len, me, o, op, ref, ref1, results, tmp; + Client.prototype._processGroupMemberships = function(infos) { + var g, groupsToSync, i, id, info, isUpdated, j, len, len1, me, o, op, ref, ref1, results, tmp; tmp = this.groups; this.groups = {}; + groupsToSync = []; for (i = 0, len = infos.length; i < len; i++) { info = infos[i]; me = { @@ -1089,35 +1161,91 @@ op = 0; g = (ref1 = tmp[o.id]) != null ? ref1 : null; if (g == null) { - g = new bit6.Group(o.id); op = 1; + g = new bit6.Group(o.id); } else { delete tmp[o.id]; } this.groups[g.id] = g; - if (g.update(o)) { + isUpdated = g.update(o); + if (isUpdated) { this.emit('group', g, op); } + this._ensureConversationForGroup(g, isUpdated); + if (isUpdated && me.role !== 'left') { + groupsToSync.push(g.id); + } } - results = []; for (id in tmp) { g = tmp[id]; - results.push(this.emit('group', g, -1)); + this.emit('group', g, -1); + } + results = []; + for (j = 0, len1 = groupsToSync.length; j < len1; j++) { + id = groupsToSync[j]; + results.push(this._loadGroupWithMembers(id, false)); } return results; }; - Client.prototype._processGroupDeltas = function(o) { - var g, op; - op = 0; - g = this.groups[o.id]; + Client.prototype._loadGroupWithMembers = function(id, reloadMembshipsOnFail, cb) { + return this.api('/groups/' + id, { + embed: 'members' + }, (function(_this) { + return function(err, result) { + _this._processGroup(id, result); + if (err && reloadMembshipsOnFail) { + return _this._loadMe(function(err) { + return typeof cb === "function" ? cb(err) : void 0; + }); + } else { + return typeof cb === "function" ? cb(err, result) : void 0; + } + }; + })(this)); + }; + + Client.prototype._processGroup = function(id, o) { + var g, i, len, m, op, ref, ref1, ref2; + g = this.groups[id]; if (g == null) { - g = new bit6.Group(o.id); + console.log('syncGroup - could not find Group in local DB', id, 'data=', o); + return; + } + op = 0; + if (o == null) { + if ((ref = g.me) != null) { + ref.role = 'left'; + } + } else { + g.update(o, true); + } + if ((ref1 = g.me) != null ? ref1.identity : void 0) { + ref2 = g.members; + for (i = 0, len = ref2.length; i < len; i++) { + m = ref2[i]; + if (m.id === g.me.identity) { + g.me.role = m.role; + break; + } + } + } + this._ensureConversationForGroup(g, op >= 0); + return this.emit('group', g, op); + }; + + Client.prototype._ensureConversationForGroup = function(g, isGroupUpdated) { + var conv, convId, op; + op = 0; + convId = g.getConversationId(); + conv = this.conversations[convId]; + if (!conv) { + this.conversations[convId] = conv = new bit6.Conversation(convId); + conv.updated = g.updated; op = 1; - this.groups[g.id] = g; } - if (g.update(o)) { - return this.emit('group', g, op); + if (op || isGroupUpdated) { + return this.emit('conversation', conv, op); } }; @@ -1147,8 +1275,8 @@ msg.data = data; } m = { - 'to': to, - 'body': JSON.stringify(msg) + to: to, + body: JSON.stringify(msg) }; if ((ref = this.rpc) != null) { ref.call('verto.info', { @@ -1160,8 +1288,16 @@ return true; }; - Client.prototype.startCall = function(other, opts) { - return this._createDialog(true, other, opts); + Client.prototype.startCall = function(to, opts) { + return this._createDialog(true, to, opts); + }; + + Client.prototype.startPhoneCall = function(phone) { + var to; + to = 'pstn:' + phone; + return this.startCall(to, { + audio: true + }); }; Client.prototype.findDialogByCallID = function(callID) { @@ -1200,35 +1336,40 @@ return null; }; - Client.prototype.deleteDialog = function(d) { - var c, i, idx, len, ref; + Client.prototype._deleteDialog = function(d) { + var c, i, idx, len, ref, ref1; ref = this.dialogs; for (idx = i = 0, len = ref.length; i < len; idx = ++i) { c = ref[idx]; if (c === d) { this.dialogs.splice(idx, 1); - return; + break; + } + } + if (this.dialogs.length === 0) { + if ((ref1 = this.capture) != null) { + ref1.stop(); } + return this.capture = null; } }; Client.prototype._createDialog = function(outgoing, other, opts) { var c; c = this.findDialogByOther(other); - if (c != null) { - return null; + if (c) { + return c; } c = new bit6.Dialog(this, outgoing, other, opts); this.dialogs.push(c); c.on('error', (function(_this) { - return function() { - return console.log('Dialog error: ', c); + return function(msg) { + return console.log('Dialog error: ', c, msg); }; })(this)); c.on('end', (function(_this) { return function() { - console.log('Dialog end in Main: ', c); - return _this.deleteDialog(c); + return _this._deleteDialog(c); }; })(this)); return c; @@ -1242,36 +1383,30 @@ return new bit6.Rtc(); }; - Client.prototype._createRtcMedia = function() { - return new bit6.RtcMedia(); + Client.prototype._createRtcCapture = function() { + return new bit6.RtcCapture(); }; - Client.prototype._ensureRtcMedia = function(opts, delta, cb) { - if (!this.media && delta > 0) { - this.media = this._createRtcMedia(); - this.media.init(opts); - this.media.start(); - } - if (this.media) { - this.media.counter += delta; - if (this.media.counter > 0) { - if (cb != null) { - return this.media.check(cb); - } - } else { - this.media.stop(); - this.media = null; - if (cb != null) { - return cb(true); - } - } + Client.prototype._ensureRtcCapture = function(opts, cb) { + if (!this.capture) { + this.capture = this._createRtcCapture(); + this.capture.on('video', (function(_this) { + return function(v, op) { + return _this._emitVideoEvent(v, null, op); + }; + })(this)); } + return this.capture.request(opts, cb); + }; + + Client.prototype._emitVideoEvent = function(v, d, op) { + return this.emit('video', v, d, op); }; Client.prototype.getNameFromIdentity = function(ident) { - var g, r, ref, ref1, ref2, t; + var g, m, r, ref, ref1, t; t = ident; - if (t == null) { + if (ident == null) { console.log('getNameFromId null', ident); return null; } @@ -1281,17 +1416,45 @@ } switch (r[0]) { case 'usr': + case 'pstn': t = r[1]; break; case 'grp': g = this.getGroup(ident); - t = (ref = g != null ? (ref1 = g.group) != null ? (ref2 = ref1.meta) != null ? ref2.title : void 0 : void 0 : void 0) != null ? ref : t; + if (!g) { + console.log('Group not found: ' + ident); + t = 'Group not found'; + } else { + t = (ref = (ref1 = g.meta) != null ? ref1.title : void 0) != null ? ref : null; + if (!t && g.members.length) { + r = (function() { + var i, len, ref2, results; + ref2 = g.members; + results = []; + for (i = 0, len = ref2.length; i < len; i++) { + m = ref2[i]; + if (m.role === 'user' || m.role === 'admin') { + results.push(this.getNameFromIdentity(m.id)); + } + } + return results; + }).call(this); + if (r.length < 4) { + t = r.join(', '); + } else { + t = r[0] + ', ' + r[1] + ' + ' + (r.length - 2) + ' more'; + } + } + if (t == null) { + t = 'Untitled Group'; + } + } } return t; }; Client.prototype._handleRtMessage = function(m) { - var g, gid, i, len, o, old, p, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, ref8; + var g, gid, i, len, old, p, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, ref8, results; switch (m.type) { case 'push': return this._handlePushRtMessage(m.data); @@ -1301,13 +1464,12 @@ } if (((ref1 = m.data) != null ? (ref2 = ref1.groups) != null ? ref2.length : void 0 : void 0) > 0) { ref3 = m.data.groups; + results = []; for (i = 0, len = ref3.length; i < len; i++) { - o = ref3[i]; - this._loadGroupWithMembers(o.id); + g = ref3[i]; + results.push(this._loadGroupWithMembers(g.id, true)); } - return this._loadMe(function(err) { - return console.log('LoadMe on Group update done', err); - }); + return results; } break; case 'presence': @@ -1370,7 +1532,9 @@ case 0x0300: case 0x0400: return this._loadMe(function(err) { - return console.log('LoadMsgDeltas on push done', err); + if (err) { + return console.log('LoadMsgDeltas on push done', err); + } }); default: return console.log('Unknown push: ', d); @@ -1389,8 +1553,8 @@ this._handleRtMessage(x); } catch (error) { ex = error; - console.log('Exception parsing JSON response verto.info', ex); - console.log(' -- RAW {{{', params.msg.body, '}}}'); + console.log('Exception parsing JSON response verto.info ' + ex); + console.log(' -- RAW {{{' + params.msg.body + '}}}'); } } break; @@ -1872,7 +2036,6 @@ }; Outgoing.prototype.send = function(cb) { - console.log('Sending: ', this); if (!this.hasAttachment()) { this.client._processMessage(this); return this.client._sendMessagePost(this, cb); @@ -1881,16 +2044,15 @@ return function(err) { _this.client._processMessage(_this); if (err != null) { - return cb(err); + return typeof cb === "function" ? cb(err) : void 0; } return _this._getUploadParams(function(err, params) { - console.log('Got upload params', params, 'err=', err); if (err != null) { - return cb(err); + return typeof cb === "function" ? cb(err) : void 0; } return _this._uploadAttachmentAndThumbnail(params, function(err) { if (err != null) { - return cb(err); + return typeof cb === "function" ? cb(err) : void 0; } return _this.client._sendMessagePost(_this, cb); }); @@ -1915,7 +2077,9 @@ Outgoing.prototype._loadAttachmentAndThumbnail = function(cb) { return bit6.Transfer.readFileAsArrayBuffer(this.attachFile, (function(_this) { return function(err, info, data) { - console.log('Read file ', info, 'err=', err); + if (err) { + console.log('Read file ', info, 'err=', err); + } if (err != null) { return cb(err); } @@ -1931,7 +2095,9 @@ x = params.uploads.attach; return bit6.Outgoing.uploadFile(x.endpoint, x.params, f, (function(_this) { return function(err) { - console.log('Main attach uploaded err=', err); + if (err) { + console.log('Main attach uploaded err=', err); + } if (err != null) { return cb(err); } @@ -1941,7 +2107,9 @@ } x = params.uploads.thumb; return bit6.Outgoing.uploadFile(x.endpoint, x.params, _this.thumbBlob, function(err) { - console.log('Thumb uploaded err=', err); + if (err) { + console.log('Thumb uploaded err=', err); + } if (err != null) { return cb(err); } @@ -2011,7 +2179,9 @@ (window.URL || window.webkitURL).revokeObjectURL(media.src); return bit6.Outgoing.createThumbnail(media, (function(_this) { return function(err, thumbDataUrl) { - console.log('Thumb created err=', err); + if (err) { + console.log('Thumb created err=', err); + } if (err != null) { return cb(err); } @@ -2034,7 +2204,6 @@ maxHeight = 320; tw = (ref = media != null ? media.videoWidth : void 0) != null ? ref : media.width; th = (ref1 = media != null ? media.videoHeight : void 0) != null ? ref1 : media.height; - console.log('Orig media loaded. dimen=', tw, th); if (tw > th) { if (tw > maxWidth) { th *= maxWidth / tw; @@ -2071,7 +2240,6 @@ xhr = new XMLHttpRequest(); xhr.open('POST', endpoint, true); xhr.onload = function(e) { - console.log('xhr complete status=' + xhr.status + ' ' + xhr.statusText); if (xhr.status >= 200 && xhr.status < 300) { return cb(null); } else { @@ -2231,130 +2399,261 @@ var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; - bit6.RtcMedia = (function(superClass) { - extend(RtcMedia, superClass); + bit6.RtcCapture = (function(superClass) { + extend(RtcCapture, superClass); - function RtcMedia() { - return RtcMedia.__super__.constructor.apply(this, arguments); + function RtcCapture() { + RtcCapture.__super__.constructor.apply(this, arguments); + this.options = { + audio: false, + video: false, + screen: false + }; + this.preparingScreen = false; + this.preparingMedia = false; + this.errorScreen = null; + this.errorMedia = null; + this.cbs = []; + this.localStream = null; + this.localEl = null; + this.localScreenStream = null; } - RtcMedia.prototype.init = function(options) { - this.options = options; - this.preparing = true; - this.ok = true; - this.cbs = []; - return this.counter = 0; + RtcCapture.prototype.getStreams = function(opts) { + var arr; + arr = []; + if (opts.audio || opts.video) { + if (this.localStream) { + arr.push(this.localStream); + } + } + if (opts.screen) { + if (this.localScreenStream) { + arr.push(this.localScreenStream); + } + } + return arr; }; - RtcMedia.prototype.start = function() { - this.localStream = null; - this.localEl = null; - console.log('RtcMedia start', this.options); - return RtcMedia.getUserMedia(this.options, (function(_this) { - return function(stream) { - _this._handleUserMedia(stream); - return _this._done(true); - }; - })(this), (function(_this) { + RtcCapture.prototype.request = function(opts, cb) { + return this._prepareScreenSharing(opts != null ? opts.screen : void 0, (function(_this) { return function(err) { - console.log('getUserMedia error: ', err); - return _this._done(false); + var newAudio, newVideo, ref, ref1; + if (err) { + console.log('RtcCapture.request: Could not get ScreenSharing', err); + } + newAudio = (ref = opts.audio) != null ? ref : _this.options.audio; + newVideo = (ref1 = opts.video) != null ? ref1 : _this.options.video; + return _this._prepareCameraMic(newAudio, newVideo, function(err2) { + if (err2) { + console.log('RtcCapture.request: Could not get audio/video', err2); + } + return _this._check(cb); + }); }; })(this)); }; - RtcMedia.prototype.check = function(cb) { - if (this.preparing) { - return this.cbs.push(cb); - } else { - return cb(this.ok); + RtcCapture.prototype._check = function(cb) { + var cb2, err, i, len, results, x; + if (this.preparingScreen || this.preparingMedia) { + this.cbs.push(cb); + return; } - }; - - RtcMedia.prototype._done = function(ok) { - var cb, i, len, results, x; - this.preparing = false; - this.ok = ok; - x = this.cbs; - this.cbs = []; - results = []; - for (i = 0, len = x.length; i < len; i++) { - cb = x[i]; - results.push(cb(this.ok)); + err = this.errorMedia; + if (!err && !this.localStream) { + err = this.errorScreen; + } + cb(err); + if (this.cbs.length > 0) { + x = this.cbs; + this.cbs = []; + results = []; + for (i = 0, len = x.length; i < len; i++) { + cb2 = x[i]; + results.push(cb2(err)); + } + return results; } - return results; }; - RtcMedia.prototype.stop = function() { - var ref, ref1; + RtcCapture.prototype.stop = function() { + var e; + if (this.localScreenStream) { + bit6.Rtc.stopMediaStream(this.localScreenStream); + this.localScreenStream = null; + } if (this.localStream) { - this.localStream.stop(); + bit6.Rtc.stopMediaStream(this.localStream); this.localStream = null; } if (this.localEl) { - this.localEl.src = ''; - if (!this.options.localMediaEl) { - if ((ref = this.localEl) != null) { - if ((ref1 = ref.parentNode) != null) { - ref1.removeChild(this.localEl); - } - } + e = this.localEl; + this.localEl = null; + if (e.src != null) { + e.src = ''; } - return this.localEl = null; + this.emit('video', e, -1); } + return this.removeAllListeners(); }; - RtcMedia.prototype._handleUserMedia = function(stream) { - var e, ref; - this.localStream = stream; - e = (ref = this.options.localMediaEl) != null ? ref : null; - if (!e && this.options.video) { - e = document.createElement('video'); - e.setAttribute('class', 'local'); - e.setAttribute('autoplay', 'true'); - e.setAttribute('muted', 'true'); - e.muted = true; - this.options.containerEl.appendChild(e); - } - this.localEl = e; - if (e) { - return this.localEl = RtcMedia.attachMediaStream(e, stream); + RtcCapture.prototype._getScreenSharingOpts = function(cb) { + var onmsg, opts; + if ((typeof navigator !== "undefined" && navigator !== null ? navigator.mozGetUserMedia : void 0) != null) { + opts = { + video: { + mozMediaSource: 'window', + mediaSource: 'window' + } + }; + return cb(null, opts); } + onmsg = function(msg) { + var d; + d = msg != null ? msg.data : void 0; + console.log('WebApp.gotMessage', d); + if ((d != null ? d.state : void 0) !== 'completed') { + return; + } + console.log('WebRTC onmsg done'); + window.removeEventListener('message', onmsg, false); + opts = { + audio: false, + video: { + mandatory: { + chromeMediaSource: 'desktop', + chromeMediaSourceId: msg.data.streamId, + maxWidth: window.screen.width, + maxHeight: window.screen.height + } + } + }; + return cb(null, opts); + }; + window.addEventListener('message', onmsg, false); + return window.postMessage({ + requestId: 100, + data: ['screen', 'window'] + }, '*'); }; - RtcMedia.getUserMedia = function(opts, success, error) { - if ((typeof window !== "undefined" && window !== null ? window.getUserMedia : void 0) != null) { - return window.getUserMedia(opts, success, error); + RtcCapture.prototype._prepareScreenSharing = function(flag, cb) { + if (flag === false && this.localScreenStream) { + this.localScreenStream = null; + return cb(null); } - if ((typeof navigator !== "undefined" && navigator !== null ? navigator.getUserMedia : void 0) != null) { - return navigator.getUserMedia(opts, success, error); + if (!flag === true) { + return cb(null); } - if ((typeof navigator !== "undefined" && navigator !== null ? navigator.mozGetUserMedia : void 0) != null) { - return navigator.mozGetUserMedia(opts, success, error); + this.options.screen = true; + if (this.localScreenStream) { + return cb(null); } - if ((typeof navigator !== "undefined" && navigator !== null ? navigator.webkitGetUserMedia : void 0) != null) { - return navigator.webkitGetUserMedia(opts, success, error); + if (this.preparingScreen) { + return cb(null); } - return error('WebRTC not supported. Could not find getUserMedia()'); + this.preparingScreen = true; + return this._getScreenSharingOpts((function(_this) { + return function(err, opts) { + if (err != null) { + _this.preparingScreen = false; + _this.errorScreen = err; + return cb(err); + } + return RtcCapture.getUserMedia(opts, function(err, stream) { + _this.preparingScreen = false; + if (err != null) { + _this.errorScreen = err; + } + if (err == null) { + _this.localScreenStream = stream; + } + return cb(err); + }); + }; + })(this)); }; - RtcMedia.attachMediaStream = function(elem, stream) { - if ((typeof window !== "undefined" && window !== null ? window.attachMediaStream : void 0) != null) { - return window.attachMediaStream(elem, stream); + RtcCapture.prototype._prepareCameraMic = function(audio, video, cb) { + var opts; + if (this.options.audio === audio && this.options.video === video) { + return cb(null); } - if ((elem != null ? elem.srcObject : void 0) != null) { - elem.srcObject = stream; - } else if ((elem != null ? elem.mozSrcObject : void 0) != null) { - elem.mozSrcObject = stream; - } else if ((elem != null ? elem.src : void 0) != null) { - elem.src = window.URL.createObjectURL(stream); - } else { - console.log('Error attaching stream to element', elem); + if (this.preparingMedia) { + return cb(null); } - return elem; + if (!audio && !video) { + this.options.audio = this.options.video = false; + this._handleLocalCameraMicStream(null); + return cb(null); + } + this.options.audio = audio; + this.options.video = video; + opts = { + audio: this.options.audio, + video: this.options.video + }; + this.preparingMedia = true; + return RtcCapture.getUserMedia(opts, (function(_this) { + return function(err, stream) { + _this.preparingMedia = false; + if (err != null) { + _this.errorMedia = err; + } + _this._handleLocalCameraMicStream(stream); + return cb(err); + }; + })(this)); }; - return RtcMedia; + RtcCapture.prototype._handleLocalCameraMicStream = function(s) { + var e, olds, ref; + olds = this.localStream; + this.localStream = s; + if (olds && olds !== s) { + bit6.Rtc.stopMediaStream(olds); + } + e = (ref = this.localEl) != null ? ref : null; + if (this.options.video && s !== olds) { + if (!e) { + e = document.createElement('video'); + e.setAttribute('autoplay', 'true'); + e.setAttribute('muted', 'true'); + e.muted = true; + this.emit('video', e, 1); + } + return this.localEl = bit6.Rtc.attachMediaStream(e, s); + } else if (!this.options.video && (e != null)) { + this.localEl = null; + e.src = ''; + return this.emit('video', e, -1); + } + }; + + RtcCapture.getUserMedia = function(opts, cb) { + var fn, n, ref, ref1, w; + w = window; + n = navigator; + fn = null; + if (!fn && (w != null ? w.getUserMedia : void 0)) { + fn = w.getUserMedia.bind(w); + } + if (!fn && n) { + fn = (ref = (ref1 = n.getUserMedia) != null ? ref1 : n.mozGetUserMedia) != null ? ref : n.webkitGetUserMedia; + if (fn) { + fn = fn.bind(n); + } + } + if (fn == null) { + return cb('WebRTC not supported. Could not find getUserMedia()'); + } + return fn(opts, function(stream) { + return cb(null, stream); + }, cb); + }; + + return RtcCapture; })(bit6.EventEmitter); @@ -2371,35 +2670,47 @@ return Rtc.__super__.constructor.apply(this, arguments); } - Rtc.prototype.init = function(media, outgoing, iceServers, options) { - this.media = media; + Rtc.prototype.init = function(outgoing, iceServers) { this.outgoing = outgoing; - this.options = options; this.pcConstraints = { optional: [ { - 'DtlsSrtpKeyAgreement': true + DtlsSrtpKeyAgreement: true } ] }; - this.pcConfig = this._createPcConfig(iceServers); - this.isStarted = false; - this.remoteStream = null; + this.pcConfig = { + iceServers: iceServers + }; this.pc = null; + this.remoteEls = {}; this.outgoingTransfer = null; this.incomingTransfer = null; this.bufferedIceCandidates = []; this.bufferedIceCandidatesDone = false; - return this.bufferedOfferAnswer = null; + this.bufferedOfferAnswer = null; + this.hadIceForAudio = false; + return this.hadIceForVideo = false; }; - Rtc.prototype.start = function() { - console.log('Rtc start', this.options); - this.remoteEl = null; - if (this.outgoing && this._preparePeerConnection()) { - if (this.options.data) { + Rtc.prototype.update = function(capture, opts, remoteOpts) { + var m, sdpOpts; + this.options = opts; + if (this.outgoing && this._preparePeerConnection(capture)) { + if (this.options.data && (this.dc == null)) { this._createDataChannel(); } + sdpOpts = {}; + if (remoteOpts.audio || remoteOpts.video) { + m = {}; + if (remoteOpts.audio) { + m.OfferToReceiveAudio = true; + } + if (remoteOpts.video) { + m.OfferToReceiveVideo = true; + } + sdpOpts.mandatory = m; + } return this.pc.createOffer((function(_this) { return function(offer) { return _this._setLocalAndSendOfferAnswer(offer); @@ -2408,53 +2719,124 @@ return function(err) { return console.log('CreateOffer error', err); }; - })(this)); + })(this), sdpOpts); } }; Rtc.prototype.stop = function() { - var ref, ref1; - this.isStarted = false; - if (this.remoteStream) { - this.remoteStream = null; - } - if (this.remoteEl) { - this.remoteEl.src = ''; - if (!this.options.remoteMediaEl) { - if ((ref = this.remoteEl) != null) { - if ((ref1 = ref.parentNode) != null) { - ref1.removeChild(this.remoteEl); - } - } + var e, id, ref, ref1, ref2; + ref = this.remoteEls; + for (id in ref) { + e = ref[id]; + if (e.src != null) { + e.src = ''; } - this.remoteEl = null; + this._removeDomElement(e); } - if (this.dc) { - this.dc.close(); - this.dc = null; + this.remoteEls = {}; + if ((ref1 = this.dc) != null) { + ref1.close(); } - if (this.pc) { - this.pc.close(); - return this.pc = null; + this.dc = null; + if ((ref2 = this.pc) != null) { + ref2.close(); } + return this.pc = null; }; - Rtc.prototype._preparePeerConnection = function() { - var ref; - if (this.isStarted) { - return false; + Rtc.prototype.getRemoteTrackKinds = function() { + var hasAudio, hasVideo, i, j, len, len1, ref, ref1, ref2, s, ss, t; + hasVideo = false; + hasAudio = false; + ss = (ref = (ref1 = this.pc) != null ? typeof ref1.getRemoteStreams === "function" ? ref1.getRemoteStreams() : void 0 : void 0) != null ? ref : []; + for (i = 0, len = ss.length; i < len; i++) { + s = ss[i]; + ref2 = s.getTracks(); + for (j = 0, len1 = ref2.length; j < len1; j++) { + t = ref2[j]; + if (t.kind === 'video' && !t.muted) { + hasVideo = true; + } + if (t.kind === 'audio') { + hasAudio = true; + } + } + } + return { + audio: hasAudio, + video: hasVideo + }; + }; + + Rtc.prototype._preparePeerConnection = function(capture) { + var localStreams; + if (this.pc == null) { + this.pc = this._createPeerConnection(); } - this.pc = this._createPeerConnection(); if (this.pc == null) { return false; } - if ((ref = this.media) != null ? ref.localStream : void 0) { - this.pc.addStream(this.media.localStream); + localStreams = capture.getStreams(this.options); + if ((typeof window !== "undefined" && window !== null ? window.mozRTCPeerConnection : void 0) != null) { + this._mozSyncLocalStreams(this.pc, localStreams); + } else { + this._syncLocalStreams(this.pc, localStreams); } - this.isStarted = true; return true; }; + Rtc.prototype._syncLocalStreams = function(pc, localStreams) { + var i, j, k, len, len1, ref, results, s, toRemove; + toRemove = {}; + ref = pc.getLocalStreams(); + for (i = 0, len = ref.length; i < len; i++) { + s = ref[i]; + toRemove[s.id] = s; + } + for (j = 0, len1 = localStreams.length; j < len1; j++) { + s = localStreams[j]; + if (toRemove[s.id]) { + delete toRemove[s.id]; + } else { + pc.addStream(s); + } + } + results = []; + for (k in toRemove) { + s = toRemove[k]; + results.push(pc.removeStream(s)); + } + return results; + }; + + Rtc.prototype._mozSyncLocalStreams = function(pc, localStreams) { + var i, j, k, l, len, len1, len2, ref, ref1, results, s, t, toRemove; + toRemove = {}; + ref = pc.getSenders(); + for (i = 0, len = ref.length; i < len; i++) { + s = ref[i]; + toRemove[s.track.id] = s; + } + for (j = 0, len1 = localStreams.length; j < len1; j++) { + s = localStreams[j]; + ref1 = s.getTracks(); + for (l = 0, len2 = ref1.length; l < len2; l++) { + t = ref1[l]; + if (toRemove[t.id]) { + delete toRemove[t.id]; + } else { + pc.addTrack(t, s); + } + } + } + results = []; + for (k in toRemove) { + s = toRemove[k]; + results.push(pc.removeTrack(s)); + } + return results; + }; + Rtc.prototype._createPeerConnection = function() { var PeerConnection, error, ex, pc, ref, ref1; try { @@ -2462,22 +2844,32 @@ pc = new PeerConnection(this.pcConfig, this.pcConstraints); pc.onicecandidate = (function(_this) { return function(evt) { - return _this._handleIceCandidate(evt); + return _this._handleIceCandidate(evt.candidate); + }; + })(this); + pc.ondatachannel = (function(_this) { + return function(evt) { + return _this._createDataChannel(evt.channel); }; })(this); pc.onaddstream = (function(_this) { return function(evt) { - return _this._handleRemoteStreamAdded(evt); + return _this._handleRemoteStreamAdded(evt.stream); }; })(this); pc.onremovestream = (function(_this) { return function(evt) { - return _this._handleRemoteStreamRemoved(evt); + return _this._handleRemoteStreamRemoved(evt.stream); }; })(this); - pc.ondatachannel = (function(_this) { + pc.onaddtrack = (function(_this) { return function(evt) { - return _this._createDataChannel(evt.channel); + return console.log('onaddtrack', evt); + }; + })(this); + pc.onremovetrack = (function(_this) { + return function(evt) { + return console.log('onremovetrack', evt); }; })(this); return pc; @@ -2521,12 +2913,10 @@ }; Rtc.prototype._handleDcOpen = function() { - console.log("The Data Channel is Opened"); return this.emit('dcOpen'); }; Rtc.prototype._handleDcClose = function() { - console.log("The Data Channel is Closed"); if (this.outgoingTransfer || this.incomingTransfer) { return this._handleDcError('DataChannel closed'); } @@ -2576,19 +2966,16 @@ } }; - Rtc.prototype._handleIceCandidate = function(evt) { - var base, c, idx; - console.log('handleIceCandidate event: ', evt); - if (evt.candidate != null) { - c = evt.candidate; + Rtc.prototype._handleIceCandidate = function(c) { + var base, idx; + if ((c != null ? c.candidate : void 0) != null) { idx = c.sdpMLineIndex; if ((base = this.bufferedIceCandidates)[idx] == null) { base[idx] = []; } - return this.bufferedIceCandidates[idx].push(c); + this.bufferedIceCandidates[idx].push(c); + return this.bufferedIceCandidatesDone = false; } else { - console.log('End of candidates.'); - console.log('BufferedCandidates', this.bufferedIceCandidates); this.bufferedIceCandidatesDone = true; return this._maybeSendOfferAnswer(); } @@ -2600,24 +2987,26 @@ offerAnswer = this._mergeSdp(this.bufferedOfferAnswer, this.bufferedIceCandidates); this.bufferedOfferAnswer = null; this.bufferedIceCandidates = []; - this.bufferedIceCandidatesDone = false; - console.log('Send OfferAnswer:', offerAnswer); + if (this.options.audio) { + this.hadIceForAudio = true; + } + if (this.options.video) { + this.hadIceForVideo = true; + } + if ((this.options.audio && !this.hadIceForAudio) || (this.options.video && !this.hadIceForVideo)) { + this.bufferedIceCandidatesDone = false; + } if (offerAnswer) { return this.emit('offerAnswer', offerAnswer); } } }; - Rtc.prototype._createPcConfig = function(iceServers) { - var c; - console.log('RTC createPcConfig got ICE servers:', iceServers); - return c = { - iceServers: iceServers - }; - }; - Rtc.prototype._mergeSdp = function(offerAnswer, candidatesByMlineIndex) { var chunk, chunks, end, i, idx, len, sdp, start; + if (candidatesByMlineIndex.length === 0) { + return offerAnswer; + } sdp = offerAnswer.sdp; chunks = []; start = 0; @@ -2629,6 +3018,12 @@ sdp = ''; for (idx = i = 0, len = chunks.length; i < len; idx = ++i) { chunk = chunks[idx]; + if (chunk.indexOf('m=audio') === 0) { + this.hadIceForAudio = true; + } + if (chunk.indexOf('m=video') === 0) { + this.hadIceForVideo = true; + } sdp += chunk; if (idx > 0 && (candidatesByMlineIndex[idx - 1] != null)) { sdp += this._iceCandidatesToSdp(candidatesByMlineIndex[idx - 1]); @@ -2648,40 +3043,101 @@ return t; }; - Rtc.prototype._handleRemoteStreamAdded = function(evt) { - var e, ref, ref1; - this.remoteStream = evt.stream; - e = (ref = this.options.remoteMediaEl) != null ? ref : null; - if (!e) { - if (this.options.video) { - e = document.createElement('video'); - this.options.containerEl.appendChild(e); - } else if (this.options.audio) { - e = document.createElement('audio'); - if ((typeof window !== "undefined" && window !== null ? window.webrtcDetectedType : void 0) === 'plugin') { - if (typeof document !== "undefined" && document !== null) { - if ((ref1 = document.body) != null) { - ref1.appendChild(e); - } + Rtc.prototype._handleRemoteStreamAdded = function(s) { + var e, hasAudio, hasVideo, i, len, ref, ref1, t; + s.onaddtrack = (function(_this) { + return function(ee) { + return _this._handleRemoteTrackAdded(ee.target, ee.track); + }; + })(this); + s.onremovetrack = (function(_this) { + return function(ee) { + return _this._handleRemoteTrackRemoved(ee.target, ee.track); + }; + })(this); + hasVideo = false; + hasAudio = false; + ref = s.getTracks(); + for (i = 0, len = ref.length; i < len; i++) { + t = ref[i]; + if (t.kind === 'video' && (!t.muted)) { + hasVideo = true; + } + if (t.kind === 'audio') { + hasAudio = true; + } + } + e = null; + if (hasVideo) { + e = document.createElement('video'); + this.emit('video', e, 1); + } else if (hasAudio) { + e = document.createElement('audio'); + if ((typeof window !== "undefined" && window !== null ? window.webrtcDetectedType : void 0) === 'plugin') { + if (typeof document !== "undefined" && document !== null) { + if ((ref1 = document.body) != null) { + ref1.appendChild(e); } } } - if (e) { - e.setAttribute('class', 'remote'); - e.setAttribute('autoplay', 'true'); - } } - this.remoteEl = e; if (e) { - this.remoteEl = bit6.RtcMedia.attachMediaStream(e, evt.stream); + e.setAttribute('autoplay', 'true'); + e = bit6.Rtc.attachMediaStream(e, s); } - if (this.options.video) { - return this.emit('videos'); + return this.remoteEls[s.id] = e; + }; + + Rtc.prototype._handleRemoteStreamRemoved = function(s) { + var e; + s.onaddtrack = null; + s.onremovetrack = null; + e = this.remoteEls[s.id]; + delete this.remoteEls[s.id]; + if (e != null) { + if (e.src != null) { + e.src = ''; + } + return this._removeDomElement(e); } }; - Rtc.prototype._handleRemoteStreamRemoved = function(evt) { - return console.log('Remote stream removed:', evt); + Rtc.prototype._handleRemoteTrackAdded = function(s, t) { + if (t.kind === 'video') { + this._handleRemoteStreamRemoved(s); + return this._handleRemoteStreamAdded(s); + } + }; + + Rtc.prototype._handleRemoteTrackRemoved = function(s, t) { + if (t.kind === 'video') { + this._handleRemoteStreamRemoved(s); + return this._handleRemoteStreamAdded(s); + } + }; + + Rtc.prototype._removeDomElement = function(e) { + var i, isAudio, len, nodeName, p, ref, ref1, ref2; + nodeName = e != null ? (ref = e.nodeName) != null ? typeof ref.toLowerCase === "function" ? ref.toLowerCase() : void 0 : void 0 : void 0; + isAudio = false; + if ('object' === nodeName) { + if (e.children) { + ref1 = e.children; + for (i = 0, len = ref1.length; i < len; i++) { + p = ref1[i]; + if ('tag' === p.name && 'audio' === p.value) { + isAudio = true; + } + } + } + } else if ('audio' === nodeName) { + isAudio = true; + } + if (isAudio) { + return (ref2 = e.parentNode) != null ? typeof ref2.removeChild === "function" ? ref2.removeChild(e) : void 0 : void 0; + } else { + return this.emit('video', e, -1); + } }; Rtc.prototype._setLocalAndSendOfferAnswer = function(offerAnswer) { @@ -2700,15 +3156,13 @@ })(this)); }; - Rtc.prototype.gotRemoteOfferAnswer = function(msg) { - var SessionDescription, offerAnswer; + Rtc.prototype.gotRemoteOfferAnswer = function(msg, capture) { + var SessionDescription, offerAnswer, ref; SessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription; offerAnswer = new SessionDescription(msg); switch (msg.type) { case 'offer': - if (!this.outgoing && !this.isStarted) { - this._preparePeerConnection(); - } + this._preparePeerConnection(capture); this.pc.setRemoteDescription(offerAnswer); return this.pc.createAnswer((function(_this) { return function(answer) { @@ -2720,9 +3174,7 @@ }; })(this)); case 'answer': - if (this.isStarted) { - return this.pc.setRemoteDescription(offerAnswer); - } + return (ref = this.pc) != null ? ref.setRemoteDescription(offerAnswer) : void 0; } }; @@ -2781,6 +3233,37 @@ })(this), delay); }; + Rtc.attachMediaStream = function(elem, stream) { + if ((typeof window !== "undefined" && window !== null ? window.attachMediaStream : void 0) != null) { + return window.attachMediaStream(elem, stream); + } + if ((elem != null ? elem.srcObject : void 0) != null) { + elem.srcObject = stream; + } else if ((elem != null ? elem.mozSrcObject : void 0) != null) { + elem.mozSrcObject = stream; + } else if ((elem != null ? elem.src : void 0) != null) { + elem.src = window.URL.createObjectURL(stream); + } else { + console.log('Error attaching stream to element', elem); + } + return elem; + }; + + Rtc.stopMediaStream = function(s) { + var i, len, ref, results, t; + if (s.stop) { + return s.stop(); + } else if (s.getTracks) { + ref = s.getTracks(); + results = []; + for (i = 0, len = ref.length; i < len; i++) { + t = ref[i]; + results.push(t != null ? t.stop() : void 0); + } + return results; + } + }; + return Rtc; })(bit6.EventEmitter); @@ -2940,7 +3423,6 @@ if (claimsStr != null) { claims = JSON.parse(bit6.Session.base64urlDecode(claimsStr)); } - console.log('Jwt claims', claims); } catch (error) { ex = error; console.log('Error parsing Jwt claims', r[1]); @@ -3011,6 +3493,31 @@ return (this.progress * 100 / this.total).toFixed(2); }; + Transfer.prototype.json = function(o) { + var arr, i, j, k, ref, ref1, t; + if (o) { + this.info.type = 'application/json'; + t = JSON.stringify(o); + arr = new Uint8Array(t.length); + for (i = j = 0, ref = t.length; 0 <= ref ? j < ref : j > ref; i = 0 <= ref ? ++j : --j) { + arr[i] = t.charCodeAt(i); + } + this.info.size = arr.byteLength; + this.data = arr.buffer; + } else { + if (this.info.type === !'application/json') { + return null; + } + arr = new Uint8Array(this.data); + t = ''; + for (i = k = 0, ref1 = arr.length; 0 <= ref1 ? k < ref1 : k > ref1; i = 0 <= ref1 ? ++k : --k) { + t += String.fromCharCode(arr[i]); + } + o = JSON.parse(t); + } + return o; + }; + Transfer.prototype._ensureSourceData = function(cb) { if (this.data != null) { return cb(null); @@ -3096,7 +3603,7 @@ rawLength = raw.length; ab = new ArrayBuffer(rawLength); arr = new Uint8Array(ab); - for (i = j = 0, ref = rawLength - 1; 0 <= ref ? j < ref : j > ref; i = 0 <= ref ? ++j : --j) { + for (i = j = 0, ref = rawLength; 0 <= ref ? j < ref : j > ref; i = 0 <= ref ? ++j : --j) { arr[i] = raw.charCodeAt(i); } raw = ab; diff --git a/www/index-bit6.js b/www/index-bit6.js index d469264..85048d5 100644 --- a/www/index-bit6.js +++ b/www/index-bit6.js @@ -1,6 +1,7 @@ cordova.require("com.bit6.sdk.Bit6SDK"); -cordova.require("com.bit6.sdk.Bit6RtcAdapter"); -cordova.require("com.bit6.sdk.Bit6RtcMediaAdapter"); +cordova.require("com.bit6.sdk.MyRtc"); +cordova.require("com.bit6.sdk.MyCapture"); +cordova.require("com.bit6.sdk.MySurface"); var phonertc = cordova.require("com.bit6.sdk.PhoneRTC"); exports.init = function(opts) { @@ -11,13 +12,24 @@ exports.init = function(opts) { // Init native WebRTC component? // Check for PeerConnection instead? var hasWebRTC = navigator.getUserMedia || navigator.mozGetUserMedia || navigator.webkitGetUserMedia || window.getUserMedia; + // WebView does not support WebRTC, init a native substitute if (!hasWebRTC) { - // WebView does not support WebRTC, init a native substitute + // Internal helper for showing/hiding OpenGL surface for native video views + var surface = new bit6.MySurface(phonertc); + b6._createRtc = function() { - return new bit6.Rtc2(phonertc); + return new bit6.MyRtc(phonertc); + }; + b6._createRtcCapture = function() { + return new bit6.MyCapture(phonertc); }; - b6._createRtcMedia = function() { - return new bit6.RtcMedia2(phonertc); + b6._emitVideoEvent = function(v, d, op) { + // Default - emit 'video' event to manage DOM + // attachment + b6.emit('video', v, d, op); + // Pass it into Surface - it will get the container element + //console.log('ToSurface: ' + v + ' parent ' + v.parentNode); + surface.onVideo(v, d, op); }; } @@ -76,6 +88,19 @@ function initPushService(b6) { }); }; + var sendIOSPushkeyToServer = function(key) { + //For iOS adding prefix p_/d_ to the push token for Bit6 server to use correct APNS + phonertc.isApnsProduction(function(isApnsProduction) { + var pushkey = key; + if (plat == 'ios') { + pushkey = isApnsProduction ? 'p_' + key : 'd_' + key; + } + + sendPushkeyToServer(pushkey); + }); + }; + + // Got notification from GCM // Note that the function has to be in global scope! window.onPushGCM = function(e) { @@ -162,7 +187,7 @@ function initPushService(b6) { ecb: 'onPushAPN' }; console.log('Register APN', opts); - window.plugins.pushNotification.register(sendPushkeyToServer, errh, opts); + window.plugins.pushNotification.register(sendIOSPushkeyToServer, errh, opts); } }); } diff --git a/www/my-capture.coffee b/www/my-capture.coffee new file mode 100644 index 0000000..d85907d --- /dev/null +++ b/www/my-capture.coffee @@ -0,0 +1,20 @@ +window.bit6 or = {} + +# WebRTC Capture connector +class bit6.MyCapture extends bit6.RtcCapture + constructor: (@phonertc) -> + super + + request: (opts, cb) -> + console.log 'RtcCapture2 request: ' + JSON.stringify(opts) + # Emit local video element placeholder + if opts?.video + console.log 'RtcCapture2 - create local video' + @localEl = e = document.createElement 'div' + @emit 'video', e, 1 + # Done! + cb null + + stop: -> + console.log 'RtcCapture2 stop' + super diff --git a/www/my-capture.js b/www/my-capture.js new file mode 100644 index 0000000..4eb4883 --- /dev/null +++ b/www/my-capture.js @@ -0,0 +1,36 @@ +// Generated by CoffeeScript 1.10.0 +(function() { + var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + + window.bit6 || (window.bit6 = {}); + + bit6.MyCapture = (function(superClass) { + extend(MyCapture, superClass); + + function MyCapture(phonertc) { + this.phonertc = phonertc; + MyCapture.__super__.constructor.apply(this, arguments); + } + + MyCapture.prototype.request = function(opts, cb) { + var e; + console.log('RtcCapture2 request: ' + JSON.stringify(opts)); + if (opts != null ? opts.video : void 0) { + console.log('RtcCapture2 - create local video'); + this.localEl = e = document.createElement('div'); + this.emit('video', e, 1); + } + return cb(null); + }; + + MyCapture.prototype.stop = function() { + console.log('RtcCapture2 stop'); + return MyCapture.__super__.stop.apply(this, arguments); + }; + + return MyCapture; + + })(bit6.RtcCapture); + +}).call(this); diff --git a/www/my-rtc.coffee b/www/my-rtc.coffee new file mode 100644 index 0000000..22cc355 --- /dev/null +++ b/www/my-rtc.coffee @@ -0,0 +1,98 @@ +window.bit6 or = {} + +# WebRTC connector +class bit6.MyRtc extends bit6.Rtc + constructor: (@phonertc) -> + super + + # Init + init: (@outgoing, iceServers) -> + super + + + update: (capture, opts, remoteOpts) -> + # Do not allow updating an existing session for now + return if @session + + @options = opts + console.log 'Rtc2.update' + JSON.stringify(@options) + ' outgoing=' + @outgoing + + cfg = + isInitiator: @outgoing + streams: + audio: @options.audio + video: @options.video + # TURN is usually the second server + if @pcConfig.iceServers.length > 1 + s = @pcConfig.iceServers[1] + cfg.turn = + host: s.url + username: s.username + password: s.credential + # Init PhoneRTC + @session = new @phonertc.Session cfg + # PhoneRTC wants to send a signaling message + @session.on 'sendMessage', (data) => + console.log 'Rtc2.sess.send: ' + JSON.stringify(data) + switch data.type + when 'offer', 'answer' + # from super._setLocalAndSendOfferAnswer + @bufferedOfferAnswer = + type: data.type + sdp: data.sdp + @_maybeSendOfferAnswer() + when 'candidate' + # We need 'm=' index. Seems to be in 'label' + # We patch the 'candidate' object + data.sdpMLineIndex = data.label + # Note that we wrap it into another object + # since this is what is expected in JS world + @_handleIceCandidate data + when 'IceGatheringChange' + if data.state is 'COMPLETE' + @_handleIceCandidate {} + when 'bye' + console.log ' - bye' + # Internal PhoneRTC event, emitted when + # Session has been created + when '__set_session_key' + console.log 'Rtc2 - session key set' + #cb true + + @session.on 'answer', () => + console.log 'Rtc2.sess.answer' + @session.on 'disconnect', () => + console.log 'Rtc2.sess.disconnect' + + # Prepares the PeerConnection. Calls createOffer() only if initiator + @session.call() + + # Stop media + stop: -> + console.log 'Rtc2.stop' + @session?.close() + @session = null + super + + + # Got a remote description (offer or answer) + gotRemoteOfferAnswer: (msg, capture) -> + console.log "Rtc2.gotRemoteOfferAnswer: " + msg.type + ' msg=' + JSON.stringify(msg) + ' sess=' + @session + switch msg.type + # Got remote 'offer' / 'answer' + when 'offer', 'answer' + # TODO: Remove this SDP hack when upgrading to the new WebRTC native lib + # THREE days of my life!!! + # msg.sdp = msg.sdp.replace /UDP\/TLS\/RTP\/SAVPF/g, 'RTP/SAVPF' + # Hacky way of checking if we have a remote video stream to show + if msg.sdp.indexOf('m=video') > 0 + # Set remote video element placeholder + e = document.createElement 'div' + @remoteEls['dummy'] = e + @emit 'video', e, 1 + # Pass the offer/answer to the native WebRTC lib + @session.receiveMessage msg if @session + + # Got hangup from remote + gotHangup: (msg) -> + @stop() diff --git a/www/my-rtc.js b/www/my-rtc.js new file mode 100644 index 0000000..323d7b7 --- /dev/null +++ b/www/my-rtc.js @@ -0,0 +1,118 @@ +// Generated by CoffeeScript 1.9.1 +(function() { + var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + + window.bit6 || (window.bit6 = {}); + + bit6.MyRtc = (function(superClass) { + extend(MyRtc, superClass); + + function MyRtc(phonertc) { + this.phonertc = phonertc; + MyRtc.__super__.constructor.apply(this, arguments); + } + + MyRtc.prototype.init = function(outgoing, iceServers) { + this.outgoing = outgoing; + return MyRtc.__super__.init.apply(this, arguments); + }; + + MyRtc.prototype.update = function(capture, opts, remoteOpts) { + var cfg, s; + if (this.session) { + return; + } + this.options = opts; + console.log('Rtc2.update' + JSON.stringify(this.options) + ' outgoing=' + this.outgoing); + cfg = { + isInitiator: this.outgoing, + streams: { + audio: this.options.audio, + video: this.options.video + } + }; + if (this.pcConfig.iceServers.length > 1) { + s = this.pcConfig.iceServers[1]; + cfg.turn = { + host: s.url, + username: s.username, + password: s.credential + }; + } + this.session = new this.phonertc.Session(cfg); + this.session.on('sendMessage', (function(_this) { + return function(data) { + console.log('Rtc2.sess.send: ' + JSON.stringify(data)); + switch (data.type) { + case 'offer': + case 'answer': + _this.bufferedOfferAnswer = { + type: data.type, + sdp: data.sdp + }; + return _this._maybeSendOfferAnswer(); + case 'candidate': + data.sdpMLineIndex = data.label; + return _this._handleIceCandidate(data); + case 'IceGatheringChange': + if (data.state === 'COMPLETE') { + return _this._handleIceCandidate({}); + } + break; + case 'bye': + return console.log(' - bye'); + case '__set_session_key': + return console.log('Rtc2 - session key set'); + } + }; + })(this)); + this.session.on('answer', (function(_this) { + return function() { + return console.log('Rtc2.sess.answer'); + }; + })(this)); + this.session.on('disconnect', (function(_this) { + return function() { + return console.log('Rtc2.sess.disconnect'); + }; + })(this)); + return this.session.call(); + }; + + MyRtc.prototype.stop = function() { + var ref; + console.log('Rtc2.stop'); + if ((ref = this.session) != null) { + ref.close(); + } + this.session = null; + return MyRtc.__super__.stop.apply(this, arguments); + }; + + MyRtc.prototype.gotRemoteOfferAnswer = function(msg, capture) { + var e; + console.log("Rtc2.gotRemoteOfferAnswer: " + msg.type + ' msg=' + JSON.stringify(msg) + ' sess=' + this.session); + switch (msg.type) { + case 'offer': + case 'answer': + if (msg.sdp.indexOf('m=video') > 0) { + e = document.createElement('div'); + this.remoteEls['dummy'] = e; + this.emit('video', e, 1); + } + if (this.session) { + return this.session.receiveMessage(msg); + } + } + }; + + MyRtc.prototype.gotHangup = function(msg) { + return this.stop(); + }; + + return MyRtc; + + })(bit6.Rtc); + +}).call(this); diff --git a/www/my-surface.coffee b/www/my-surface.coffee new file mode 100644 index 0000000..adc5267 --- /dev/null +++ b/www/my-surface.coffee @@ -0,0 +1,30 @@ +window.bit6 or = {} + +# Surface for rendering video views +class bit6.MySurface + constructor: (@phonertc) -> + @counter = 0 + + onVideo: (v, d, op) -> + console.log 'Surface.video v=' + v + ' d=' + d + ' o=' + op + # Added first video element + if op > 0 and @counter is 0 + @_show v.parentNode + # Removed last video element + else if op < 0 and @counter is 1 + @_hide() + # Keep track of the number of video elements + @counter += op + + _show: (container) -> + console.log 'RtcSurface show ' + container + opts = + container: container + local: + position: [0, 0] + size: [100, 100] + @phonertc.setVideoView opts + + _hide: -> + console.log 'RtcSurface hide' + @phonertc.hideVideoView() diff --git a/www/my-surface.js b/www/my-surface.js new file mode 100644 index 0000000..80cb32f --- /dev/null +++ b/www/my-surface.js @@ -0,0 +1,43 @@ +// Generated by CoffeeScript 1.10.0 +(function() { + window.bit6 || (window.bit6 = {}); + + bit6.MySurface = (function() { + function MySurface(phonertc) { + this.phonertc = phonertc; + this.counter = 0; + } + + MySurface.prototype.onVideo = function(v, d, op) { + console.log('Surface.video v=' + v + ' d=' + d + ' o=' + op); + if (op > 0 && this.counter === 0) { + this._show(v.parentNode); + } else if (op < 0 && this.counter === 1) { + this._hide(); + } + return this.counter += op; + }; + + MySurface.prototype._show = function(container) { + var opts; + console.log('RtcSurface show ' + container); + opts = { + container: container, + local: { + position: [0, 0], + size: [100, 100] + } + }; + return this.phonertc.setVideoView(opts); + }; + + MySurface.prototype._hide = function() { + console.log('RtcSurface hide'); + return this.phonertc.hideVideoView(); + }; + + return MySurface; + + })(); + +}).call(this); diff --git a/www/phonertc.js b/www/phonertc.js index 1e5b6a8..9c01df7 100644 --- a/www/phonertc.js +++ b/www/phonertc.js @@ -201,4 +201,15 @@ exports.hideVideoView = function () { exports.showVideoView = function () { exec(null, null, 'PhoneRTCPlugin', 'showVideoView', []); -}; \ No newline at end of file +}; + +function isApnsProduction(onSuccess) { + if (cordova.platformId === 'ios') { //This is needed only for ios. + exec(onSuccess, null, 'PhoneRTCPlugin', 'isApnsProduction', []); + } + else { + onSuccess(false); + } +} + +exports.isApnsProduction = isApnsProduction;