+
diff --git a/imports/ui/video/video.js b/imports/ui/video/video.js
new file mode 100644
index 0000000..3c01097
--- /dev/null
+++ b/imports/ui/video/video.js
@@ -0,0 +1,165 @@
+import { Meteor } from 'meteor/meteor';
+import { Template } from 'meteor/templating';
+import { ReactiveDict } from 'meteor/reactive-dict';
+import { ReactiveVar } from 'meteor/reactive-var';
+import { Router } from 'meteor/iron:router';
+import { Mongo } from 'meteor/mongo';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { IonPopup } from 'meteor/meteoric:ionic';
+import { $ } from 'meteor/jquery';
+
+import { Citoyens } from '../../api/citoyens.js';
+
+import './video.html';
+
+import { pageVideo } from '../../api/client/reactive.js';
+
+Template.videoRTC.onCreated(function() {
+ const template = Template.instance();
+ template.ready = new ReactiveVar(false);
+
+ this.autorun(function() {
+ const handle = Meteor.subscribe('callUsers');
+ if (handle.ready()) {
+ template.ready.set(handle.ready());
+ }
+ });
+});
+
+Template.videoRTC.onRendered(function() {
+ const self = this;
+ Meteor.VideoCallServices.onError = (err, data) => {
+ console.log(err, data);
+ };
+ Meteor.VideoCallServices.onTargetAccept = () => {
+ console.log('onTargetAccept');
+ };
+
+ Meteor.VideoCallServices.onPeerConnectionCreated = () => {
+ console.log('onPeerConnectionCreated');
+ };
+
+});
+
+Template.videoRTC.events({
+ 'click .callUser'(event, instance) {
+ event.preventDefault();
+ const user = Meteor.users.findOne({
+ _id: this.user._id._str,
+ });
+ if (!user || !user.status.online) { throw new Meteor.Error(500, 'user offline'); }
+ pageVideo.set('showCaller', true);
+ pageVideo.set('showChat', this.user._id._str);
+ },
+});
+
+Template.videoRTC.helpers({
+ listUsers() {
+ return Citoyens.findOne({ _id: new Mongo.ObjectID(Meteor.userId()) }).listFollows();
+ },
+ onlineUser(userId) {
+ const user = Meteor.users.findOne({
+ _id: userId,
+ });
+ if (!user || !user.status.online) {
+ return false;
+ }
+ return true;
+ },
+ showChat() {
+ return pageVideo.get('showChat');
+ },
+ showTarget() {
+ return pageVideo.get('showTarget');
+ },
+ showCaller() {
+ return pageVideo.get('showCaller');
+ },
+ dataReady() {
+ return Template.instance().ready.get();
+ },
+});
+
+Template.callTargetRTC.onRendered(function() {
+ const self = this;
+
+ this.autorun(function(c) {
+ if (pageVideo.get('showTarget') === true) {
+ self.caller = self.$('#caller').get(0);
+ self.target = self.$('#target').get(0);
+ Meteor.VideoCallServices.answerPhoneCall(self.caller, self.target);
+ c.stop();
+ }
+ });
+
+ Meteor.VideoCallServices.onTerminateCall = () => {
+ console.log('onTerminateCall');
+ IonPopup.alert({
+ title: TAPi18n.__('onTerminateCall'),
+ });
+ Meteor.VideoCallServices.endPhoneCall();
+ const stopStreamedVideo = (videoElem) => {
+ const stream = videoElem.srcObject;
+ if (stream) {
+ const tracks = stream.getTracks();
+ tracks.forEach(function(track) {
+ track.stop();
+ });
+ videoElem.srcObject = null;
+ }
+ };
+ stopStreamedVideo(self.caller);
+ stopStreamedVideo(self.target);
+ pageVideo.set('showChat', false);
+ pageVideo.set('showCaller', false);
+ pageVideo.set('showTarget', false);
+ };
+});
+
+Template.callCallerRTC.onRendered(function() {
+ const self = this;
+
+ this.autorun(function(c) {
+ if (pageVideo.get('showCaller') === true) {
+ self.caller = self.$('#caller').get(0);
+ self.target = self.$('#target').get(0);
+ if (self.caller && self.target) {
+ Meteor.VideoCallServices.call(pageVideo.get('showChat'), self.caller, self.target);
+ c.stop();
+ }
+ }
+ });
+
+ Meteor.VideoCallServices.onTerminateCall = () => {
+ console.log('onTerminateCall');
+ IonPopup.alert({
+ title: TAPi18n.__('onTerminateCall'),
+ });
+ Meteor.VideoCallServices.endPhoneCall();
+ const stopStreamedVideo = (videoElem) => {
+ const stream = videoElem.srcObject;
+ if (stream) {
+ const tracks = stream.getTracks();
+ tracks.forEach(function(track) {
+ track.stop();
+ });
+ videoElem.srcObject = null;
+ }
+ };
+ stopStreamedVideo(self.caller);
+ stopStreamedVideo(self.target);
+ pageVideo.set('showChat', false);
+ pageVideo.set('showCaller', false);
+ pageVideo.set('showTarget', false);
+ };
+});
+
+Template.callRTC.events({
+ 'click .endPhoneCall'(event, instance) {
+ event.preventDefault();
+ Meteor.VideoCallServices.endPhoneCall();
+ pageVideo.set('showChat', false);
+ pageVideo.set('showCaller', false);
+ pageVideo.set('showTarget', false);
+ },
+});
diff --git a/mobile-config.js b/mobile-config.js
index 7eb2b16..d855511 100644
--- a/mobile-config.js
+++ b/mobile-config.js
@@ -4,8 +4,8 @@ App.info({
description: 'communecter mobile',
author: 'thomas',
email: 'thomas.craipeau@gmail.com',
- version: '0.0.11',
- buildNumber: '111'
+ version: '0.0.13',
+ buildNumber: '113'
});
App.setPreference('android-targetSdkVersion', '23');
@@ -62,6 +62,7 @@ App.setPreference('StatusBarOverlaysWebView', 'false');
App.setPreference('StatusBarBackgroundColor', '#324553');
App.setPreference('Orientation', 'portrait');
+
App.accessRule('*');
App.accessRule('http://*');
App.accessRule('https://*');
diff --git a/package.json b/package.json
index 9cbf9be..19dc833 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "communecter",
- "version": "0.0.11",
+ "version": "0.0.13",
"description": "communecter",
"private": true,
"scripts": {
diff --git a/packages/elmarti_video-chat/.gitignore b/packages/elmarti_video-chat/.gitignore
new file mode 100644
index 0000000..30468e4
--- /dev/null
+++ b/packages/elmarti_video-chat/.gitignore
@@ -0,0 +1 @@
+.npm
\ No newline at end of file
diff --git a/packages/elmarti_video-chat/.travis.yml b/packages/elmarti_video-chat/.travis.yml
new file mode 100644
index 0000000..5125066
--- /dev/null
+++ b/packages/elmarti_video-chat/.travis.yml
@@ -0,0 +1,8 @@
+sudo: required
+language: node_js
+node_js:
+ - "0.10"
+before_install:
+ - "curl -L http://git.io/ejPSng | /bin/sh"
+env:
+ - TEST_COMMAND=meteor
\ No newline at end of file
diff --git a/packages/elmarti_video-chat/LICENSE b/packages/elmarti_video-chat/LICENSE
new file mode 100644
index 0000000..5284c43
--- /dev/null
+++ b/packages/elmarti_video-chat/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 elmarti
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/elmarti_video-chat/README.md b/packages/elmarti_video-chat/README.md
new file mode 100644
index 0000000..4651036
--- /dev/null
+++ b/packages/elmarti_video-chat/README.md
@@ -0,0 +1,72 @@
+# Meteor Video Chat
+This extension allows you to implement user-to-user video calling in React, Angular and Blaze.
+
+
+[Example](https://meteorvideochat.herokuapp.com) - Click register to create a user
+
+[](http://waffle.io/elmarti/meteor-video-chat)
+[](https://travis-ci.org/elmarti/meteor-video-chat)
+## Configuration
+Here you can set the [RTCConfiguration](https://developer.mozilla.org/en-US/docs/Web/API/RTCConfiguration). If you are testing outside of a LAN, you'll need to procure some [STUN & TURN](https://gist.github.com/yetithefoot/7592580) servers.
+
+```
+Meteor.VideoCallServices.RTCConfiguration = [{'iceServers': [{
+ 'urls': 'stun:stun.example.org'
+ }]
+}];
+```
+#### Calling a user
+To call a user, call the following method with their _id, the local video element/ react ref and the target video/react ref.
+```
+Meteor.VideoCallServices.call(targetUserId, this.refs.caller, this.refs.target);
+```
+#### Deciding who can connect to whom
+The follow method can be overridden on the server side to implement some kind of filtering. Returning `false` will cancel the call, and `true` will allow it to go ahead.
+```
+Meteor.VideoCallServices.checkConnect = function(caller, target){
+return *can caller and target call each other"
+};
+```
+#### Answering a call
+The first step is to handle the onReceivePhoneCall callback and then to accept the call. The answerPhoneCall method accepts the local video and the target video.
+```
+ Meteor.VideoCallServices.onReceivePhoneCall = (userId) => {
+Meteor.VideoCallServices.answerPhoneCall(this.refs.caller, this.refs.target);
+ };
+
+```
+#### Ending phone call
+Simply call
+```
+Meteor.VideoCallServices.end();
+```
+#### Other events
+The following method is invoked when the callee accepts the phone call.
+```
+Meteor.VideoCallServices.onTargetAccept = () => {
+}
+```
+The following method is invoked when either user ends the call
+```
+Meteor.VideoCallServices.onTerminateCall = () => {
+}
+```
+The following method invoked when the RTCPeerConnection instance has been created, making it possible to consitently mutate it or add a data channel
+```
+Meteor.VideoCallServices.onPeerConnectionCreated = () => {
+}
+
+```
+The following method is invoked on both the client and server whenever an error is caught.
+User is only passed on the server
+
+```
+Meteor.VideoCallServices.onError = (err, data, user) => {
+}
+```
+
+This project is sponsored by the following:
+
+[](https://www.browserstack.com/)
+
+[](https://sentry.io/)
\ No newline at end of file
diff --git a/packages/elmarti_video-chat/package.js b/packages/elmarti_video-chat/package.js
new file mode 100644
index 0000000..a130b63
--- /dev/null
+++ b/packages/elmarti_video-chat/package.js
@@ -0,0 +1,27 @@
+Package.describe({
+ name: 'elmarti:video-chat',
+ version: '1.1.1',
+ // Brief, one-line summary of the package.
+ summary: 'Simple WebRTC Video Chat for your app.',
+ // URL to the Git repository containing the source code for this package.
+ git: 'https://github.com/elmarti/meteor-video-chat',
+ // By default, Meteor will default to using README.md for documentation.
+ // To avoid submitting documentation, set this field to null.
+ documentation: 'README.md'
+});
+
+Package.onUse(api => {
+ api.versionsFrom('1.5');
+ api.use('ecmascript');
+ api.use("rocketchat:streamer@0.5.0");
+ api.use("mizzao:user-status@0.6.6");
+ api.addFiles(['services/server.js', 'services/publish.js'], "server");
+ api.addFiles(['services/client.js', 'services/adapter.js'], "client");
+});
+Package.onTest(api => {
+ api.use('ecmascript');
+ api.use('tinytest');
+
+ api.addFiles('video-chat-tests.js');
+ api.export([]);
+});
diff --git a/packages/elmarti_video-chat/services/adapter.js b/packages/elmarti_video-chat/services/adapter.js
new file mode 100644
index 0000000..e6dd693
--- /dev/null
+++ b/packages/elmarti_video-chat/services/adapter.js
@@ -0,0 +1,3976 @@
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.adapter = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0 ? 'm=' + part : part).trim() + '\r\n';
+ });
+ };
+
+// Returns lines that start with a certain prefix.
+ SDPUtils.matchPrefix = function(blob, prefix) {
+ return SDPUtils.splitLines(blob).filter(function(line) {
+ return line.indexOf(prefix) === 0;
+ });
+ };
+
+// Parses an ICE candidate line. Sample input:
+// candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8
+// rport 55996"
+ SDPUtils.parseCandidate = function(line) {
+ var parts;
+ // Parse both variants.
+ if (line.indexOf('a=candidate:') === 0) {
+ parts = line.substring(12).split(' ');
+ } else {
+ parts = line.substring(10).split(' ');
+ }
+
+ var candidate = {
+ foundation: parts[0],
+ component: parseInt(parts[1], 10),
+ protocol: parts[2].toLowerCase(),
+ priority: parseInt(parts[3], 10),
+ ip: parts[4],
+ port: parseInt(parts[5], 10),
+ // skip parts[6] == 'typ'
+ type: parts[7]
+ };
+
+ for (var i = 8; i < parts.length; i += 2) {
+ switch (parts[i]) {
+ case 'raddr':
+ candidate.relatedAddress = parts[i + 1];
+ break;
+ case 'rport':
+ candidate.relatedPort = parseInt(parts[i + 1], 10);
+ break;
+ case 'tcptype':
+ candidate.tcpType = parts[i + 1];
+ break;
+ default: // extension handling, in particular ufrag
+ candidate[parts[i]] = parts[i + 1];
+ break;
+ }
+ }
+ return candidate;
+ };
+
+// Translates a candidate object into SDP candidate attribute.
+ SDPUtils.writeCandidate = function(candidate) {
+ var sdp = [];
+ sdp.push(candidate.foundation);
+ sdp.push(candidate.component);
+ sdp.push(candidate.protocol.toUpperCase());
+ sdp.push(candidate.priority);
+ sdp.push(candidate.ip);
+ sdp.push(candidate.port);
+
+ var type = candidate.type;
+ sdp.push('typ');
+ sdp.push(type);
+ if (type !== 'host' && candidate.relatedAddress &&
+ candidate.relatedPort) {
+ sdp.push('raddr');
+ sdp.push(candidate.relatedAddress); // was: relAddr
+ sdp.push('rport');
+ sdp.push(candidate.relatedPort); // was: relPort
+ }
+ if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') {
+ sdp.push('tcptype');
+ sdp.push(candidate.tcpType);
+ }
+ if (candidate.ufrag) {
+ sdp.push('ufrag');
+ sdp.push(candidate.ufrag);
+ }
+ return 'candidate:' + sdp.join(' ');
+ };
+
+// Parses an ice-options line, returns an array of option tags.
+// a=ice-options:foo bar
+ SDPUtils.parseIceOptions = function(line) {
+ return line.substr(14).split(' ');
+ }
+
+// Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input:
+// a=rtpmap:111 opus/48000/2
+ SDPUtils.parseRtpMap = function(line) {
+ var parts = line.substr(9).split(' ');
+ var parsed = {
+ payloadType: parseInt(parts.shift(), 10) // was: id
+ };
+
+ parts = parts[0].split('/');
+
+ parsed.name = parts[0];
+ parsed.clockRate = parseInt(parts[1], 10); // was: clockrate
+ // was: channels
+ parsed.numChannels = parts.length === 3 ? parseInt(parts[2], 10) : 1;
+ return parsed;
+ };
+
+// Generate an a=rtpmap line from RTCRtpCodecCapability or
+// RTCRtpCodecParameters.
+ SDPUtils.writeRtpMap = function(codec) {
+ var pt = codec.payloadType;
+ if (codec.preferredPayloadType !== undefined) {
+ pt = codec.preferredPayloadType;
+ }
+ return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate +
+ (codec.numChannels !== 1 ? '/' + codec.numChannels : '') + '\r\n';
+ };
+
+// Parses an a=extmap line (headerextension from RFC 5285). Sample input:
+// a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
+// a=extmap:2/sendonly urn:ietf:params:rtp-hdrext:toffset
+ SDPUtils.parseExtmap = function(line) {
+ var parts = line.substr(9).split(' ');
+ return {
+ id: parseInt(parts[0], 10),
+ direction: parts[0].indexOf('/') > 0 ? parts[0].split('/')[1] : 'sendrecv',
+ uri: parts[1]
+ };
+ };
+
+// Generates a=extmap line from RTCRtpHeaderExtensionParameters or
+// RTCRtpHeaderExtension.
+ SDPUtils.writeExtmap = function(headerExtension) {
+ return 'a=extmap:' + (headerExtension.id || headerExtension.preferredId) +
+ (headerExtension.direction && headerExtension.direction !== 'sendrecv'
+ ? '/' + headerExtension.direction
+ : '') +
+ ' ' + headerExtension.uri + '\r\n';
+ };
+
+// Parses an ftmp line, returns dictionary. Sample input:
+// a=fmtp:96 vbr=on;cng=on
+// Also deals with vbr=on; cng=on
+ SDPUtils.parseFmtp = function(line) {
+ var parsed = {};
+ var kv;
+ var parts = line.substr(line.indexOf(' ') + 1).split(';');
+ for (var j = 0; j < parts.length; j++) {
+ kv = parts[j].trim().split('=');
+ parsed[kv[0].trim()] = kv[1];
+ }
+ return parsed;
+ };
+
+// Generates an a=ftmp line from RTCRtpCodecCapability or RTCRtpCodecParameters.
+ SDPUtils.writeFmtp = function(codec) {
+ var line = '';
+ var pt = codec.payloadType;
+ if (codec.preferredPayloadType !== undefined) {
+ pt = codec.preferredPayloadType;
+ }
+ if (codec.parameters && Object.keys(codec.parameters).length) {
+ var params = [];
+ Object.keys(codec.parameters).forEach(function(param) {
+ params.push(param + '=' + codec.parameters[param]);
+ });
+ line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n';
+ }
+ return line;
+ };
+
+// Parses an rtcp-fb line, returns RTCPRtcpFeedback object. Sample input:
+// a=rtcp-fb:98 nack rpsi
+ SDPUtils.parseRtcpFb = function(line) {
+ var parts = line.substr(line.indexOf(' ') + 1).split(' ');
+ return {
+ type: parts.shift(),
+ parameter: parts.join(' ')
+ };
+ };
+// Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters.
+ SDPUtils.writeRtcpFb = function(codec) {
+ var lines = '';
+ var pt = codec.payloadType;
+ if (codec.preferredPayloadType !== undefined) {
+ pt = codec.preferredPayloadType;
+ }
+ if (codec.rtcpFeedback && codec.rtcpFeedback.length) {
+ // FIXME: special handling for trr-int?
+ codec.rtcpFeedback.forEach(function(fb) {
+ lines += 'a=rtcp-fb:' + pt + ' ' + fb.type +
+ (fb.parameter && fb.parameter.length ? ' ' + fb.parameter : '') +
+ '\r\n';
+ });
+ }
+ return lines;
+ };
+
+// Parses an RFC 5576 ssrc media attribute. Sample input:
+// a=ssrc:3735928559 cname:something
+ SDPUtils.parseSsrcMedia = function(line) {
+ var sp = line.indexOf(' ');
+ var parts = {
+ ssrc: parseInt(line.substr(7, sp - 7), 10)
+ };
+ var colon = line.indexOf(':', sp);
+ if (colon > -1) {
+ parts.attribute = line.substr(sp + 1, colon - sp - 1);
+ parts.value = line.substr(colon + 1);
+ } else {
+ parts.attribute = line.substr(sp + 1);
+ }
+ return parts;
+ };
+
+// Extracts the MID (RFC 5888) from a media section.
+// returns the MID or undefined if no mid line was found.
+ SDPUtils.getMid = function(mediaSection) {
+ var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:')[0];
+ if (mid) {
+ return mid.substr(6);
+ }
+ }
+
+ SDPUtils.parseFingerprint = function(line) {
+ var parts = line.substr(14).split(' ');
+ return {
+ algorithm: parts[0].toLowerCase(), // algorithm is case-sensitive in Edge.
+ value: parts[1]
+ };
+ };
+
+// Extracts DTLS parameters from SDP media section or sessionpart.
+// FIXME: for consistency with other functions this should only
+// get the fingerprint line as input. See also getIceParameters.
+ SDPUtils.getDtlsParameters = function(mediaSection, sessionpart) {
+ var lines = SDPUtils.matchPrefix(mediaSection + sessionpart,
+ 'a=fingerprint:');
+ // Note: a=setup line is ignored since we use the 'auto' role.
+ // Note2: 'algorithm' is not case sensitive except in Edge.
+ return {
+ role: 'auto',
+ fingerprints: lines.map(SDPUtils.parseFingerprint)
+ };
+ };
+
+// Serializes DTLS parameters to SDP.
+ SDPUtils.writeDtlsParameters = function(params, setupType) {
+ var sdp = 'a=setup:' + setupType + '\r\n';
+ params.fingerprints.forEach(function(fp) {
+ sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n';
+ });
+ return sdp;
+ };
+// Parses ICE information from SDP media section or sessionpart.
+// FIXME: for consistency with other functions this should only
+// get the ice-ufrag and ice-pwd lines as input.
+ SDPUtils.getIceParameters = function(mediaSection, sessionpart) {
+ var lines = SDPUtils.splitLines(mediaSection);
+ // Search in session part, too.
+ lines = lines.concat(SDPUtils.splitLines(sessionpart));
+ var iceParameters = {
+ usernameFragment: lines.filter(function(line) {
+ return line.indexOf('a=ice-ufrag:') === 0;
+ })[0].substr(12),
+ password: lines.filter(function(line) {
+ return line.indexOf('a=ice-pwd:') === 0;
+ })[0].substr(10)
+ };
+ return iceParameters;
+ };
+
+// Serializes ICE parameters to SDP.
+ SDPUtils.writeIceParameters = function(params) {
+ return 'a=ice-ufrag:' + params.usernameFragment + '\r\n' +
+ 'a=ice-pwd:' + params.password + '\r\n';
+ };
+
+// Parses the SDP media section and returns RTCRtpParameters.
+ SDPUtils.parseRtpParameters = function(mediaSection) {
+ var description = {
+ codecs: [],
+ headerExtensions: [],
+ fecMechanisms: [],
+ rtcp: []
+ };
+ var lines = SDPUtils.splitLines(mediaSection);
+ var mline = lines[0].split(' ');
+ for (var i = 3; i < mline.length; i++) { // find all codecs from mline[3..]
+ var pt = mline[i];
+ var rtpmapline = SDPUtils.matchPrefix(
+ mediaSection, 'a=rtpmap:' + pt + ' ')[0];
+ if (rtpmapline) {
+ var codec = SDPUtils.parseRtpMap(rtpmapline);
+ var fmtps = SDPUtils.matchPrefix(
+ mediaSection, 'a=fmtp:' + pt + ' ');
+ // Only the first a=fmtp: is considered.
+ codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {};
+ codec.rtcpFeedback = SDPUtils.matchPrefix(
+ mediaSection, 'a=rtcp-fb:' + pt + ' ')
+ .map(SDPUtils.parseRtcpFb);
+ description.codecs.push(codec);
+ // parse FEC mechanisms from rtpmap lines.
+ switch (codec.name.toUpperCase()) {
+ case 'RED':
+ case 'ULPFEC':
+ description.fecMechanisms.push(codec.name.toUpperCase());
+ break;
+ default: // only RED and ULPFEC are recognized as FEC mechanisms.
+ break;
+ }
+ }
+ }
+ SDPUtils.matchPrefix(mediaSection, 'a=extmap:').forEach(function(line) {
+ description.headerExtensions.push(SDPUtils.parseExtmap(line));
+ });
+ // FIXME: parse rtcp.
+ return description;
+ };
+
+// Generates parts of the SDP media section describing the capabilities /
+// parameters.
+ SDPUtils.writeRtpDescription = function(kind, caps) {
+ var sdp = '';
+
+ // Build the mline.
+ sdp += 'm=' + kind + ' ';
+ sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs.
+ sdp += ' UDP/TLS/RTP/SAVPF ';
+ sdp += caps.codecs.map(function(codec) {
+ if (codec.preferredPayloadType !== undefined) {
+ return codec.preferredPayloadType;
+ }
+ return codec.payloadType;
+ }).join(' ') + '\r\n';
+
+ sdp += 'c=IN IP4 0.0.0.0\r\n';
+ sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n';
+
+ // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb.
+ caps.codecs.forEach(function(codec) {
+ sdp += SDPUtils.writeRtpMap(codec);
+ sdp += SDPUtils.writeFmtp(codec);
+ sdp += SDPUtils.writeRtcpFb(codec);
+ });
+ var maxptime = 0;
+ caps.codecs.forEach(function(codec) {
+ if (codec.maxptime > maxptime) {
+ maxptime = codec.maxptime;
+ }
+ });
+ if (maxptime > 0) {
+ sdp += 'a=maxptime:' + maxptime + '\r\n';
+ }
+ sdp += 'a=rtcp-mux\r\n';
+
+ caps.headerExtensions.forEach(function(extension) {
+ sdp += SDPUtils.writeExtmap(extension);
+ });
+ // FIXME: write fecMechanisms.
+ return sdp;
+ };
+
+// Parses the SDP media section and returns an array of
+// RTCRtpEncodingParameters.
+ SDPUtils.parseRtpEncodingParameters = function(mediaSection) {
+ var encodingParameters = [];
+ var description = SDPUtils.parseRtpParameters(mediaSection);
+ var hasRed = description.fecMechanisms.indexOf('RED') !== -1;
+ var hasUlpfec = description.fecMechanisms.indexOf('ULPFEC') !== -1;
+
+ // filter a=ssrc:... cname:, ignore PlanB-msid
+ var ssrcs = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
+ .map(function(line) {
+ return SDPUtils.parseSsrcMedia(line);
+ })
+ .filter(function(parts) {
+ return parts.attribute === 'cname';
+ });
+ var primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc;
+ var secondarySsrc;
+
+ var flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID')
+ .map(function(line) {
+ var parts = line.split(' ');
+ parts.shift();
+ return parts.map(function(part) {
+ return parseInt(part, 10);
+ });
+ });
+ if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) {
+ secondarySsrc = flows[0][1];
+ }
+
+ description.codecs.forEach(function(codec) {
+ if (codec.name.toUpperCase() === 'RTX' && codec.parameters.apt) {
+ var encParam = {
+ ssrc: primarySsrc,
+ codecPayloadType: parseInt(codec.parameters.apt, 10),
+ rtx: {
+ ssrc: secondarySsrc
+ }
+ };
+ encodingParameters.push(encParam);
+ if (hasRed) {
+ encParam = JSON.parse(JSON.stringify(encParam));
+ encParam.fec = {
+ ssrc: secondarySsrc,
+ mechanism: hasUlpfec ? 'red+ulpfec' : 'red'
+ };
+ encodingParameters.push(encParam);
+ }
+ }
+ });
+ if (encodingParameters.length === 0 && primarySsrc) {
+ encodingParameters.push({
+ ssrc: primarySsrc
+ });
+ }
+
+ // we support both b=AS and b=TIAS but interpret AS as TIAS.
+ var bandwidth = SDPUtils.matchPrefix(mediaSection, 'b=');
+ if (bandwidth.length) {
+ if (bandwidth[0].indexOf('b=TIAS:') === 0) {
+ bandwidth = parseInt(bandwidth[0].substr(7), 10);
+ } else if (bandwidth[0].indexOf('b=AS:') === 0) {
+ // use formula from JSEP to convert b=AS to TIAS value.
+ bandwidth = parseInt(bandwidth[0].substr(5), 10) * 1000 * 0.95
+ - (50 * 40 * 8);
+ } else {
+ bandwidth = undefined;
+ }
+ encodingParameters.forEach(function(params) {
+ params.maxBitrate = bandwidth;
+ });
+ }
+ return encodingParameters;
+ };
+
+// parses http://draft.ortc.org/#rtcrtcpparameters*
+ SDPUtils.parseRtcpParameters = function(mediaSection) {
+ var rtcpParameters = {};
+
+ var cname;
+ // Gets the first SSRC. Note that with RTX there might be multiple
+ // SSRCs.
+ var remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
+ .map(function(line) {
+ return SDPUtils.parseSsrcMedia(line);
+ })
+ .filter(function(obj) {
+ return obj.attribute === 'cname';
+ })[0];
+ if (remoteSsrc) {
+ rtcpParameters.cname = remoteSsrc.value;
+ rtcpParameters.ssrc = remoteSsrc.ssrc;
+ }
+
+ // Edge uses the compound attribute instead of reducedSize
+ // compound is !reducedSize
+ var rsize = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-rsize');
+ rtcpParameters.reducedSize = rsize.length > 0;
+ rtcpParameters.compound = rsize.length === 0;
+
+ // parses the rtcp-mux attrіbute.
+ // Note that Edge does not support unmuxed RTCP.
+ var mux = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-mux');
+ rtcpParameters.mux = mux.length > 0;
+
+ return rtcpParameters;
+ };
+
+// parses either a=msid: or a=ssrc:... msid lines and returns
+// the id of the MediaStream and MediaStreamTrack.
+ SDPUtils.parseMsid = function(mediaSection) {
+ var parts;
+ var spec = SDPUtils.matchPrefix(mediaSection, 'a=msid:');
+ if (spec.length === 1) {
+ parts = spec[0].substr(7).split(' ');
+ return {stream: parts[0], track: parts[1]};
+ }
+ var planB = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
+ .map(function(line) {
+ return SDPUtils.parseSsrcMedia(line);
+ })
+ .filter(function(parts) {
+ return parts.attribute === 'msid';
+ });
+ if (planB.length > 0) {
+ parts = planB[0].value.split(' ');
+ return {stream: parts[0], track: parts[1]};
+ }
+ };
+
+// Generate a session ID for SDP.
+// https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-20#section-5.2.1
+// recommends using a cryptographically random +ve 64-bit value
+// but right now this should be acceptable and within the right range
+ SDPUtils.generateSessionId = function() {
+ return Math.random().toString().substr(2, 21);
+ };
+
+// Write boilder plate for start of SDP
+// sessId argument is optional - if not supplied it will
+// be generated randomly
+// sessVersion is optional and defaults to 2
+ SDPUtils.writeSessionBoilerplate = function(sessId, sessVer) {
+ var sessionId;
+ var version = sessVer !== undefined ? sessVer : 2;
+ if (sessId) {
+ sessionId = sessId;
+ } else {
+ sessionId = SDPUtils.generateSessionId();
+ }
+ // FIXME: sess-id should be an NTP timestamp.
+ return 'v=0\r\n' +
+ 'o=thisisadapterortc ' + sessionId + ' ' + version + ' IN IP4 127.0.0.1\r\n' +
+ 's=-\r\n' +
+ 't=0 0\r\n';
+ };
+
+ SDPUtils.writeMediaSection = function(transceiver, caps, type, stream) {
+ var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps);
+
+ // Map ICE parameters (ufrag, pwd) to SDP.
+ sdp += SDPUtils.writeIceParameters(
+ transceiver.iceGatherer.getLocalParameters());
+
+ // Map DTLS parameters to SDP.
+ sdp += SDPUtils.writeDtlsParameters(
+ transceiver.dtlsTransport.getLocalParameters(),
+ type === 'offer' ? 'actpass' : 'active');
+
+ sdp += 'a=mid:' + transceiver.mid + '\r\n';
+
+ if (transceiver.direction) {
+ sdp += 'a=' + transceiver.direction + '\r\n';
+ } else if (transceiver.rtpSender && transceiver.rtpReceiver) {
+ sdp += 'a=sendrecv\r\n';
+ } else if (transceiver.rtpSender) {
+ sdp += 'a=sendonly\r\n';
+ } else if (transceiver.rtpReceiver) {
+ sdp += 'a=recvonly\r\n';
+ } else {
+ sdp += 'a=inactive\r\n';
+ }
+
+ if (transceiver.rtpSender) {
+ // spec.
+ var msid = 'msid:' + stream.id + ' ' +
+ transceiver.rtpSender.track.id + '\r\n';
+ sdp += 'a=' + msid;
+
+ // for Chrome.
+ sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+ ' ' + msid;
+ if (transceiver.sendEncodingParameters[0].rtx) {
+ sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
+ ' ' + msid;
+ sdp += 'a=ssrc-group:FID ' +
+ transceiver.sendEncodingParameters[0].ssrc + ' ' +
+ transceiver.sendEncodingParameters[0].rtx.ssrc +
+ '\r\n';
+ }
+ }
+ // FIXME: this should be written by writeRtpDescription.
+ sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+ ' cname:' + SDPUtils.localCName + '\r\n';
+ if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) {
+ sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
+ ' cname:' + SDPUtils.localCName + '\r\n';
+ }
+ return sdp;
+ };
+
+// Gets the direction from the mediaSection or the sessionpart.
+ SDPUtils.getDirection = function(mediaSection, sessionpart) {
+ // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv.
+ var lines = SDPUtils.splitLines(mediaSection);
+ for (var i = 0; i < lines.length; i++) {
+ switch (lines[i]) {
+ case 'a=sendrecv':
+ case 'a=sendonly':
+ case 'a=recvonly':
+ case 'a=inactive':
+ return lines[i].substr(2);
+ default:
+ // FIXME: What should happen here?
+ }
+ }
+ if (sessionpart) {
+ return SDPUtils.getDirection(sessionpart);
+ }
+ return 'sendrecv';
+ };
+
+ SDPUtils.getKind = function(mediaSection) {
+ var lines = SDPUtils.splitLines(mediaSection);
+ var mline = lines[0].split(' ');
+ return mline[0].substr(2);
+ };
+
+ SDPUtils.isRejected = function(mediaSection) {
+ return mediaSection.split(' ', 2)[1] === '0';
+ };
+
+// Expose public methods.
+ module.exports = SDPUtils;
+
+},{}],2:[function(require,module,exports){
+ (function (global){
+ /*
+ * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree.
+ */
+ /* eslint-env node */
+
+ 'use strict';
+
+ var adapterFactory = require('./adapter_factory.js');
+ module.exports = adapterFactory({window: global.window});
+
+ }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{"./adapter_factory.js":3}],3:[function(require,module,exports){
+ /*
+ * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree.
+ */
+ /* eslint-env node */
+
+ 'use strict';
+
+// Shimming starts here.
+ module.exports = function(dependencies, opts) {
+ var window = dependencies && dependencies.window;
+
+ var options = {
+ shimChrome: true,
+ shimFirefox: true,
+ shimEdge: true,
+ shimSafari: true,
+ };
+
+ for (var key in opts) {
+ if (hasOwnProperty.call(opts, key)) {
+ options[key] = opts[key];
+ }
+ }
+
+ // Utils.
+ var utils = require('./utils');
+ var logging = utils.log;
+ var browserDetails = utils.detectBrowser(window);
+
+ // Export to the adapter global object visible in the browser.
+ var adapter = {
+ browserDetails: browserDetails,
+ extractVersion: utils.extractVersion,
+ disableLog: utils.disableLog,
+ disableWarnings: utils.disableWarnings
+ };
+
+ // Uncomment the line below if you want logging to occur, including logging
+ // for the switch statement below. Can also be turned on in the browser via
+ // adapter.disableLog(false), but then logging from the switch statement below
+ // will not appear.
+ // require('./utils').disableLog(false);
+
+ // Browser shims.
+ var chromeShim = require('./chrome/chrome_shim') || null;
+ var edgeShim = require('./edge/edge_shim') || null;
+ var firefoxShim = require('./firefox/firefox_shim') || null;
+ var safariShim = require('./safari/safari_shim') || null;
+
+ // Shim browser if found.
+ switch (browserDetails.browser) {
+ case 'chrome':
+ if (!chromeShim || !chromeShim.shimPeerConnection ||
+ !options.shimChrome) {
+ logging('Chrome shim is not included in this adapter release.');
+ return adapter;
+ }
+ logging('adapter.js shimming chrome.');
+ // Export to the adapter global object visible in the browser.
+ adapter.browserShim = chromeShim;
+
+ chromeShim.shimGetUserMedia(window);
+ chromeShim.shimMediaStream(window);
+ utils.shimCreateObjectURL(window);
+ chromeShim.shimSourceObject(window);
+ chromeShim.shimPeerConnection(window);
+ chromeShim.shimOnTrack(window);
+ chromeShim.shimAddTrackRemoveTrack(window);
+ chromeShim.shimGetSendersWithDtmf(window);
+ break;
+ case 'firefox':
+ if (!firefoxShim || !firefoxShim.shimPeerConnection ||
+ !options.shimFirefox) {
+ logging('Firefox shim is not included in this adapter release.');
+ return adapter;
+ }
+ logging('adapter.js shimming firefox.');
+ // Export to the adapter global object visible in the browser.
+ adapter.browserShim = firefoxShim;
+
+ firefoxShim.shimGetUserMedia(window);
+ utils.shimCreateObjectURL(window);
+ firefoxShim.shimSourceObject(window);
+ firefoxShim.shimPeerConnection(window);
+ firefoxShim.shimOnTrack(window);
+ break;
+ case 'edge':
+ if (!edgeShim || !edgeShim.shimPeerConnection || !options.shimEdge) {
+ logging('MS edge shim is not included in this adapter release.');
+ return adapter;
+ }
+ logging('adapter.js shimming edge.');
+ // Export to the adapter global object visible in the browser.
+ adapter.browserShim = edgeShim;
+
+ edgeShim.shimGetUserMedia(window);
+ utils.shimCreateObjectURL(window);
+ edgeShim.shimPeerConnection(window);
+ edgeShim.shimReplaceTrack(window);
+ break;
+ case 'safari':
+ if (!safariShim || !options.shimSafari) {
+ logging('Safari shim is not included in this adapter release.');
+ return adapter;
+ }
+ logging('adapter.js shimming safari.');
+ // Export to the adapter global object visible in the browser.
+ adapter.browserShim = safariShim;
+ // shim window.URL.createObjectURL Safari (technical preview)
+ utils.shimCreateObjectURL(window);
+ safariShim.shimRTCIceServerUrls(window);
+ safariShim.shimCallbacksAPI(window);
+ safariShim.shimLocalStreamsAPI(window);
+ safariShim.shimRemoteStreamsAPI(window);
+ safariShim.shimGetUserMedia(window);
+ break;
+ default:
+ logging('Unsupported browser!');
+ break;
+ }
+
+ return adapter;
+ };
+
+},{"./chrome/chrome_shim":4,"./edge/edge_shim":6,"./firefox/firefox_shim":9,"./safari/safari_shim":11,"./utils":12}],4:[function(require,module,exports){
+
+ /*
+ * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree.
+ */
+ /* eslint-env node */
+ 'use strict';
+ var utils = require('../utils.js');
+ var logging = utils.log;
+
+ var chromeShim = {
+ shimMediaStream: function(window) {
+ window.MediaStream = window.MediaStream || window.webkitMediaStream;
+ },
+
+ shimOnTrack: function(window) {
+ if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in
+ window.RTCPeerConnection.prototype)) {
+ Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', {
+ get: function() {
+ return this._ontrack;
+ },
+ set: function(f) {
+ if (this._ontrack) {
+ this.removeEventListener('track', this._ontrack);
+ }
+ this.addEventListener('track', this._ontrack = f);
+ }
+ });
+ var origSetRemoteDescription =
+ window.RTCPeerConnection.prototype.setRemoteDescription;
+ window.RTCPeerConnection.prototype.setRemoteDescription = function() {
+ var pc = this;
+ if (!pc._ontrackpoly) {
+ pc._ontrackpoly = function(e) {
+ // onaddstream does not fire when a track is added to an existing
+ // stream. But stream.onaddtrack is implemented so we use that.
+ e.stream.addEventListener('addtrack', function(te) {
+ var receiver;
+ if (window.RTCPeerConnection.prototype.getReceivers) {
+ receiver = pc.getReceivers().find(function(r) {
+ return r.track && r.track.id === te.track.id;
+ });
+ } else {
+ receiver = {track: te.track};
+ }
+
+ var event = new Event('track');
+ event.track = te.track;
+ event.receiver = receiver;
+ event.streams = [e.stream];
+ pc.dispatchEvent(event);
+ });
+ e.stream.getTracks().forEach(function(track) {
+ var receiver;
+ if (window.RTCPeerConnection.prototype.getReceivers) {
+ receiver = pc.getReceivers().find(function(r) {
+ return r.track && r.track.id === track.id;
+ });
+ } else {
+ receiver = {track: track};
+ }
+ var event = new Event('track');
+ event.track = track;
+ event.receiver = receiver;
+ event.streams = [e.stream];
+ pc.dispatchEvent(event);
+ });
+ };
+ pc.addEventListener('addstream', pc._ontrackpoly);
+ }
+ return origSetRemoteDescription.apply(pc, arguments);
+ };
+ }
+ },
+
+ shimGetSendersWithDtmf: function(window) {
+ // Overrides addTrack/removeTrack, depends on shimAddTrackRemoveTrack.
+ if (typeof window === 'object' && window.RTCPeerConnection &&
+ !('getSenders' in window.RTCPeerConnection.prototype) &&
+ 'createDTMFSender' in window.RTCPeerConnection.prototype) {
+ var shimSenderWithDtmf = function(pc, track) {
+ return {
+ track: track,
+ get dtmf() {
+ if (this._dtmf === undefined) {
+ if (track.kind === 'audio') {
+ this._dtmf = pc.createDTMFSender(track);
+ } else {
+ this._dtmf = null;
+ }
+ }
+ return this._dtmf;
+ },
+ _pc: pc
+ };
+ };
+
+ // augment addTrack when getSenders is not available.
+ if (!window.RTCPeerConnection.prototype.getSenders) {
+ window.RTCPeerConnection.prototype.getSenders = function() {
+ this._senders = this._senders || [];
+ return this._senders.slice(); // return a copy of the internal state.
+ };
+ var origAddTrack = window.RTCPeerConnection.prototype.addTrack;
+ window.RTCPeerConnection.prototype.addTrack = function(track, stream) {
+ var pc = this;
+ var sender = origAddTrack.apply(pc, arguments);
+ if (!sender) {
+ sender = shimSenderWithDtmf(pc, track);
+ pc._senders.push(sender);
+ }
+ return sender;
+ };
+
+ var origRemoveTrack = window.RTCPeerConnection.prototype.removeTrack;
+ window.RTCPeerConnection.prototype.removeTrack = function(sender) {
+ var pc = this;
+ origRemoveTrack.apply(pc, arguments);
+ var idx = pc._senders.indexOf(sender);
+ if (idx !== -1) {
+ pc._senders.splice(idx, 1);
+ }
+ };
+ }
+ var origAddStream = window.RTCPeerConnection.prototype.addStream;
+ window.RTCPeerConnection.prototype.addStream = function(stream) {
+ var pc = this;
+ pc._senders = pc._senders || [];
+ origAddStream.apply(pc, [stream]);
+ stream.getTracks().forEach(function(track) {
+ pc._senders.push(shimSenderWithDtmf(pc, track));
+ });
+ };
+
+ var origRemoveStream = window.RTCPeerConnection.prototype.removeStream;
+ window.RTCPeerConnection.prototype.removeStream = function(stream) {
+ var pc = this;
+ pc._senders = pc._senders || [];
+ origRemoveStream.apply(pc, [(pc._streams[stream.id] || stream)]);
+
+ stream.getTracks().forEach(function(track) {
+ var sender = pc._senders.find(function(s) {
+ return s.track === track;
+ });
+ if (sender) {
+ pc._senders.splice(pc._senders.indexOf(sender), 1); // remove sender
+ }
+ });
+ };
+ } else if (typeof window === 'object' && window.RTCPeerConnection &&
+ 'getSenders' in window.RTCPeerConnection.prototype &&
+ 'createDTMFSender' in window.RTCPeerConnection.prototype &&
+ window.RTCRtpSender &&
+ !('dtmf' in window.RTCRtpSender.prototype)) {
+ var origGetSenders = window.RTCPeerConnection.prototype.getSenders;
+ window.RTCPeerConnection.prototype.getSenders = function() {
+ var pc = this;
+ var senders = origGetSenders.apply(pc, []);
+ senders.forEach(function(sender) {
+ sender._pc = pc;
+ });
+ return senders;
+ };
+
+ Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', {
+ get: function() {
+ if (this._dtmf === undefined) {
+ if (this.track.kind === 'audio') {
+ this._dtmf = this._pc.createDTMFSender(this.track);
+ } else {
+ this._dtmf = null;
+ }
+ }
+ return this._dtmf;
+ }
+ });
+ }
+ },
+
+ shimSourceObject: function(window) {
+ var URL = window && window.URL;
+
+ if (typeof window === 'object') {
+ if (window.HTMLMediaElement &&
+ !('srcObject' in window.HTMLMediaElement.prototype)) {
+ // Shim the srcObject property, once, when HTMLMediaElement is found.
+ Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', {
+ get: function() {
+ return this._srcObject;
+ },
+ set: function(stream) {
+ var self = this;
+ // Use _srcObject as a private property for this shim
+ this._srcObject = stream;
+ if (this.src) {
+ URL.revokeObjectURL(this.src);
+ }
+
+ if (!stream) {
+ this.src = '';
+ return undefined;
+ }
+ this.src = URL.createObjectURL(stream);
+ // We need to recreate the blob url when a track is added or
+ // removed. Doing it manually since we want to avoid a recursion.
+ stream.addEventListener('addtrack', function() {
+ if (self.src) {
+ URL.revokeObjectURL(self.src);
+ }
+ self.src = URL.createObjectURL(stream);
+ });
+ stream.addEventListener('removetrack', function() {
+ if (self.src) {
+ URL.revokeObjectURL(self.src);
+ }
+ self.src = URL.createObjectURL(stream);
+ });
+ }
+ });
+ }
+ }
+ },
+
+ shimAddTrackRemoveTrack: function(window) {
+ // shim addTrack and removeTrack.
+ if (window.RTCPeerConnection.prototype.addTrack) {
+ return;
+ }
+
+ // also shim pc.getLocalStreams when addTrack is shimmed
+ // to return the original streams.
+ var origGetLocalStreams = window.RTCPeerConnection.prototype
+ .getLocalStreams;
+ window.RTCPeerConnection.prototype.getLocalStreams = function() {
+ var self = this;
+ var nativeStreams = origGetLocalStreams.apply(this);
+ self._reverseStreams = self._reverseStreams || {};
+ return nativeStreams.map(function(stream) {
+ return self._reverseStreams[stream.id];
+ });
+ };
+
+ var origAddStream = window.RTCPeerConnection.prototype.addStream;
+ window.RTCPeerConnection.prototype.addStream = function(stream) {
+ var pc = this;
+ pc._streams = pc._streams || {};
+ pc._reverseStreams = pc._reverseStreams || {};
+
+ stream.getTracks().forEach(function(track) {
+ var alreadyExists = pc.getSenders().find(function(s) {
+ return s.track === track;
+ });
+ if (alreadyExists) {
+ throw new DOMException('Track already exists.',
+ 'InvalidAccessError');
+ }
+ });
+ // Add identity mapping for consistency with addTrack.
+ // Unless this is being used with a stream from addTrack.
+ if (!pc._reverseStreams[stream.id]) {
+ var newStream = new window.MediaStream(stream.getTracks());
+ pc._streams[stream.id] = newStream;
+ pc._reverseStreams[newStream.id] = stream;
+ stream = newStream;
+ }
+ origAddStream.apply(pc, [stream]);
+ };
+
+ var origRemoveStream = window.RTCPeerConnection.prototype.removeStream;
+ window.RTCPeerConnection.prototype.removeStream = function(stream) {
+ var pc = this;
+ pc._streams = pc._streams || {};
+ pc._reverseStreams = pc._reverseStreams || {};
+
+ origRemoveStream.apply(pc, [(pc._streams[stream.id] || stream)]);
+ delete pc._reverseStreams[(pc._streams[stream.id] ?
+ pc._streams[stream.id].id : stream.id)];
+ delete pc._streams[stream.id];
+ };
+
+ window.RTCPeerConnection.prototype.addTrack = function(track, stream) {
+ var pc = this;
+ if (pc.signalingState === 'closed') {
+ throw new DOMException(
+ 'The RTCPeerConnection\'s signalingState is \'closed\'.',
+ 'InvalidStateError');
+ }
+ var streams = [].slice.call(arguments, 1);
+ if (streams.length !== 1 ||
+ !streams[0].getTracks().find(function(t) {
+ return t === track;
+ })) {
+ // this is not fully correct but all we can manage without
+ // [[associated MediaStreams]] internal slot.
+ throw new DOMException(
+ 'The adapter.js addTrack polyfill only supports a single ' +
+ ' stream which is associated with the specified track.',
+ 'NotSupportedError');
+ }
+
+ var alreadyExists = pc.getSenders().find(function(s) {
+ return s.track === track;
+ });
+ if (alreadyExists) {
+ throw new DOMException('Track already exists.',
+ 'InvalidAccessError');
+ }
+
+ pc._streams = pc._streams || {};
+ pc._reverseStreams = pc._reverseStreams || {};
+ var oldStream = pc._streams[stream.id];
+ if (oldStream) {
+ // this is using odd Chrome behaviour, use with caution:
+ // https://bugs.chromium.org/p/webrtc/issues/detail?id=7815
+ // Note: we rely on the high-level addTrack/dtmf shim to
+ // create the sender with a dtmf sender.
+ oldStream.addTrack(track);
+ pc.dispatchEvent(new Event('negotiationneeded'));
+ } else {
+ var newStream = new window.MediaStream([track]);
+ pc._streams[stream.id] = newStream;
+ pc._reverseStreams[newStream.id] = stream;
+ pc.addStream(newStream);
+ }
+ return pc.getSenders().find(function(s) {
+ return s.track === track;
+ });
+ };
+
+ window.RTCPeerConnection.prototype.removeTrack = function(sender) {
+ var pc = this;
+ if (pc.signalingState === 'closed') {
+ throw new DOMException(
+ 'The RTCPeerConnection\'s signalingState is \'closed\'.',
+ 'InvalidStateError');
+ }
+ // We can not yet check for sender instanceof RTCRtpSender
+ // since we shim RTPSender. So we check if sender._pc is set.
+ if (!sender._pc) {
+ throw new DOMException('Argument 1 of RTCPeerConnection.removeTrack ' +
+ 'does not implement interface RTCRtpSender.', 'TypeError');
+ }
+ var isLocal = sender._pc === pc;
+ if (!isLocal) {
+ throw new DOMException('Sender was not created by this connection.',
+ 'InvalidAccessError');
+ }
+
+ // Search for the native stream the senders track belongs to.
+ pc._streams = pc._streams || {};
+ var stream;
+ Object.keys(pc._streams).forEach(function(streamid) {
+ var hasTrack = pc._streams[streamid].getTracks().find(function(track) {
+ return sender.track === track;
+ });
+ if (hasTrack) {
+ stream = pc._streams[streamid];
+ }
+ });
+
+ if (stream) {
+ if (stream.getTracks().length === 1) {
+ // if this is the last track of the stream, remove the stream. This
+ // takes care of any shimmed _senders.
+ pc.removeStream(stream);
+ } else {
+ // relying on the same odd chrome behaviour as above.
+ stream.removeTrack(sender.track);
+ }
+ pc.dispatchEvent(new Event('negotiationneeded'));
+ }
+ };
+ },
+
+ shimPeerConnection: function(window) {
+ var browserDetails = utils.detectBrowser(window);
+
+ // The RTCPeerConnection object.
+ if (!window.RTCPeerConnection) {
+ window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+ // Translate iceTransportPolicy to iceTransports,
+ // see https://code.google.com/p/webrtc/issues/detail?id=4869
+ // this was fixed in M56 along with unprefixing RTCPeerConnection.
+ logging('PeerConnection');
+ if (pcConfig && pcConfig.iceTransportPolicy) {
+ pcConfig.iceTransports = pcConfig.iceTransportPolicy;
+ }
+
+ return new window.webkitRTCPeerConnection(pcConfig, pcConstraints);
+ };
+ window.RTCPeerConnection.prototype =
+ window.webkitRTCPeerConnection.prototype;
+ // wrap static methods. Currently just generateCertificate.
+ if (window.webkitRTCPeerConnection.generateCertificate) {
+ Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
+ get: function() {
+ return window.webkitRTCPeerConnection.generateCertificate;
+ }
+ });
+ }
+ } else {
+ // migrate from non-spec RTCIceServer.url to RTCIceServer.urls
+ var OrigPeerConnection = window.RTCPeerConnection;
+ window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+ if (pcConfig && pcConfig.iceServers) {
+ var newIceServers = [];
+ for (var i = 0; i < pcConfig.iceServers.length; i++) {
+ var server = pcConfig.iceServers[i];
+ if (!server.hasOwnProperty('urls') &&
+ server.hasOwnProperty('url')) {
+ utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls');
+ server = JSON.parse(JSON.stringify(server));
+ server.urls = server.url;
+ newIceServers.push(server);
+ } else {
+ newIceServers.push(pcConfig.iceServers[i]);
+ }
+ }
+ pcConfig.iceServers = newIceServers;
+ }
+ return new OrigPeerConnection(pcConfig, pcConstraints);
+ };
+ window.RTCPeerConnection.prototype = OrigPeerConnection.prototype;
+ // wrap static methods. Currently just generateCertificate.
+ Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
+ get: function() {
+ return OrigPeerConnection.generateCertificate;
+ }
+ });
+ }
+
+ var origGetStats = window.RTCPeerConnection.prototype.getStats;
+ window.RTCPeerConnection.prototype.getStats = function(selector,
+ successCallback, errorCallback) {
+ var self = this;
+ var args = arguments;
+
+ // If selector is a function then we are in the old style stats so just
+ // pass back the original getStats format to avoid breaking old users.
+ if (arguments.length > 0 && typeof selector === 'function') {
+ return origGetStats.apply(this, arguments);
+ }
+
+ // When spec-style getStats is supported, return those when called with
+ // either no arguments or the selector argument is null.
+ if (origGetStats.length === 0 && (arguments.length === 0 ||
+ typeof arguments[0] !== 'function')) {
+ return origGetStats.apply(this, []);
+ }
+
+ var fixChromeStats_ = function(response) {
+ var standardReport = {};
+ var reports = response.result();
+ reports.forEach(function(report) {
+ var standardStats = {
+ id: report.id,
+ timestamp: report.timestamp,
+ type: {
+ localcandidate: 'local-candidate',
+ remotecandidate: 'remote-candidate'
+ }[report.type] || report.type
+ };
+ report.names().forEach(function(name) {
+ standardStats[name] = report.stat(name);
+ });
+ standardReport[standardStats.id] = standardStats;
+ });
+
+ return standardReport;
+ };
+
+ // shim getStats with maplike support
+ var makeMapStats = function(stats) {
+ return new Map(Object.keys(stats).map(function(key) {
+ return [key, stats[key]];
+ }));
+ };
+
+ if (arguments.length >= 2) {
+ var successCallbackWrapper_ = function(response) {
+ args[1](makeMapStats(fixChromeStats_(response)));
+ };
+
+ return origGetStats.apply(this, [successCallbackWrapper_,
+ arguments[0]]);
+ }
+
+ // promise-support
+ return new Promise(function(resolve, reject) {
+ origGetStats.apply(self, [
+ function(response) {
+ resolve(makeMapStats(fixChromeStats_(response)));
+ }, reject]);
+ }).then(successCallback, errorCallback);
+ };
+
+ // add promise support -- natively available in Chrome 51
+ if (browserDetails.version < 51) {
+ ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']
+ .forEach(function(method) {
+ var nativeMethod = window.RTCPeerConnection.prototype[method];
+ window.RTCPeerConnection.prototype[method] = function() {
+ var args = arguments;
+ var self = this;
+ var promise = new Promise(function(resolve, reject) {
+ nativeMethod.apply(self, [args[0], resolve, reject]);
+ });
+ if (args.length < 2) {
+ return promise;
+ }
+ return promise.then(function() {
+ args[1].apply(null, []);
+ },
+ function(err) {
+ if (args.length >= 3) {
+ args[2].apply(null, [err]);
+ }
+ });
+ };
+ });
+ }
+
+ // promise support for createOffer and createAnswer. Available (without
+ // bugs) since M52: crbug/619289
+ if (browserDetails.version < 52) {
+ ['createOffer', 'createAnswer'].forEach(function(method) {
+ var nativeMethod = window.RTCPeerConnection.prototype[method];
+ window.RTCPeerConnection.prototype[method] = function() {
+ var self = this;
+ if (arguments.length < 1 || (arguments.length === 1 &&
+ typeof arguments[0] === 'object')) {
+ var opts = arguments.length === 1 ? arguments[0] : undefined;
+ return new Promise(function(resolve, reject) {
+ nativeMethod.apply(self, [resolve, reject, opts]);
+ });
+ }
+ return nativeMethod.apply(this, arguments);
+ };
+ });
+ }
+
+ // shim implicit creation of RTCSessionDescription/RTCIceCandidate
+ ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']
+ .forEach(function(method) {
+ var nativeMethod = window.RTCPeerConnection.prototype[method];
+ window.RTCPeerConnection.prototype[method] = function() {
+ arguments[0] = new ((method === 'addIceCandidate') ?
+ window.RTCIceCandidate :
+ window.RTCSessionDescription)(arguments[0]);
+ return nativeMethod.apply(this, arguments);
+ };
+ });
+
+ // support for addIceCandidate(null or undefined)
+ var nativeAddIceCandidate =
+ window.RTCPeerConnection.prototype.addIceCandidate;
+ window.RTCPeerConnection.prototype.addIceCandidate = function() {
+ if (!arguments[0]) {
+ if (arguments[1]) {
+ arguments[1].apply(null);
+ }
+ return Promise.resolve();
+ }
+ return nativeAddIceCandidate.apply(this, arguments);
+ };
+ }
+ };
+
+
+// Expose public methods.
+ module.exports = {
+ shimMediaStream: chromeShim.shimMediaStream,
+ shimOnTrack: chromeShim.shimOnTrack,
+ shimAddTrackRemoveTrack: chromeShim.shimAddTrackRemoveTrack,
+ shimGetSendersWithDtmf: chromeShim.shimGetSendersWithDtmf,
+ shimSourceObject: chromeShim.shimSourceObject,
+ shimPeerConnection: chromeShim.shimPeerConnection,
+ shimGetUserMedia: require('./getusermedia')
+ };
+
+},{"../utils.js":12,"./getusermedia":5}],5:[function(require,module,exports){
+ /*
+ * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree.
+ */
+ /* eslint-env node */
+ 'use strict';
+ var utils = require('../utils.js');
+ var logging = utils.log;
+
+// Expose public methods.
+ module.exports = function(window) {
+ var browserDetails = utils.detectBrowser(window);
+ var navigator = window && window.navigator;
+
+ var constraintsToChrome_ = function(c) {
+ if (typeof c !== 'object' || c.mandatory || c.optional) {
+ return c;
+ }
+ var cc = {};
+ Object.keys(c).forEach(function(key) {
+ if (key === 'require' || key === 'advanced' || key === 'mediaSource') {
+ return;
+ }
+ var r = (typeof c[key] === 'object') ? c[key] : {ideal: c[key]};
+ if (r.exact !== undefined && typeof r.exact === 'number') {
+ r.min = r.max = r.exact;
+ }
+ var oldname_ = function(prefix, name) {
+ if (prefix) {
+ return prefix + name.charAt(0).toUpperCase() + name.slice(1);
+ }
+ return (name === 'deviceId') ? 'sourceId' : name;
+ };
+ if (r.ideal !== undefined) {
+ cc.optional = cc.optional || [];
+ var oc = {};
+ if (typeof r.ideal === 'number') {
+ oc[oldname_('min', key)] = r.ideal;
+ cc.optional.push(oc);
+ oc = {};
+ oc[oldname_('max', key)] = r.ideal;
+ cc.optional.push(oc);
+ } else {
+ oc[oldname_('', key)] = r.ideal;
+ cc.optional.push(oc);
+ }
+ }
+ if (r.exact !== undefined && typeof r.exact !== 'number') {
+ cc.mandatory = cc.mandatory || {};
+ cc.mandatory[oldname_('', key)] = r.exact;
+ } else {
+ ['min', 'max'].forEach(function(mix) {
+ if (r[mix] !== undefined) {
+ cc.mandatory = cc.mandatory || {};
+ cc.mandatory[oldname_(mix, key)] = r[mix];
+ }
+ });
+ }
+ });
+ if (c.advanced) {
+ cc.optional = (cc.optional || []).concat(c.advanced);
+ }
+ return cc;
+ };
+
+ var shimConstraints_ = function(constraints, func) {
+ constraints = JSON.parse(JSON.stringify(constraints));
+ if (constraints && typeof constraints.audio === 'object') {
+ var remap = function(obj, a, b) {
+ if (a in obj && !(b in obj)) {
+ obj[b] = obj[a];
+ delete obj[a];
+ }
+ };
+ constraints = JSON.parse(JSON.stringify(constraints));
+ remap(constraints.audio, 'autoGainControl', 'googAutoGainControl');
+ remap(constraints.audio, 'noiseSuppression', 'googNoiseSuppression');
+ constraints.audio = constraintsToChrome_(constraints.audio);
+ }
+ if (constraints && typeof constraints.video === 'object') {
+ // Shim facingMode for mobile & surface pro.
+ var face = constraints.video.facingMode;
+ face = face && ((typeof face === 'object') ? face : {ideal: face});
+ var getSupportedFacingModeLies = browserDetails.version < 61;
+
+ if ((face && (face.exact === 'user' || face.exact === 'environment' ||
+ face.ideal === 'user' || face.ideal === 'environment')) &&
+ !(navigator.mediaDevices.getSupportedConstraints &&
+ navigator.mediaDevices.getSupportedConstraints().facingMode &&
+ !getSupportedFacingModeLies)) {
+ delete constraints.video.facingMode;
+ var matches;
+ if (face.exact === 'environment' || face.ideal === 'environment') {
+ matches = ['back', 'rear'];
+ } else if (face.exact === 'user' || face.ideal === 'user') {
+ matches = ['front'];
+ }
+ if (matches) {
+ // Look for matches in label, or use last cam for back (typical).
+ return navigator.mediaDevices.enumerateDevices()
+ .then(function(devices) {
+ devices = devices.filter(function(d) {
+ return d.kind === 'videoinput';
+ });
+ var dev = devices.find(function(d) {
+ return matches.some(function(match) {
+ return d.label.toLowerCase().indexOf(match) !== -1;
+ });
+ });
+ if (!dev && devices.length && matches.indexOf('back') !== -1) {
+ dev = devices[devices.length - 1]; // more likely the back cam
+ }
+ if (dev) {
+ constraints.video.deviceId = face.exact ? {exact: dev.deviceId} :
+ {ideal: dev.deviceId};
+ }
+ constraints.video = constraintsToChrome_(constraints.video);
+ logging('chrome: ' + JSON.stringify(constraints));
+ return func(constraints);
+ });
+ }
+ }
+ constraints.video = constraintsToChrome_(constraints.video);
+ }
+ logging('chrome: ' + JSON.stringify(constraints));
+ return func(constraints);
+ };
+
+ var shimError_ = function(e) {
+ return {
+ name: {
+ PermissionDeniedError: 'NotAllowedError',
+ InvalidStateError: 'NotReadableError',
+ DevicesNotFoundError: 'NotFoundError',
+ ConstraintNotSatisfiedError: 'OverconstrainedError',
+ TrackStartError: 'NotReadableError',
+ MediaDeviceFailedDueToShutdown: 'NotReadableError',
+ MediaDeviceKillSwitchOn: 'NotReadableError'
+ }[e.name] || e.name,
+ message: e.message,
+ constraint: e.constraintName,
+ toString: function() {
+ return this.name + (this.message && ': ') + this.message;
+ }
+ };
+ };
+
+ var getUserMedia_ = function(constraints, onSuccess, onError) {
+ shimConstraints_(constraints, function(c) {
+ navigator.webkitGetUserMedia(c, onSuccess, function(e) {
+ onError(shimError_(e));
+ });
+ });
+ };
+
+ navigator.getUserMedia = getUserMedia_;
+
+ // Returns the result of getUserMedia as a Promise.
+ var getUserMediaPromise_ = function(constraints) {
+ return new Promise(function(resolve, reject) {
+ navigator.getUserMedia(constraints, resolve, reject);
+ });
+ };
+
+ if (!navigator.mediaDevices) {
+ navigator.mediaDevices = {
+ getUserMedia: getUserMediaPromise_,
+ enumerateDevices: function() {
+ return new Promise(function(resolve) {
+ var kinds = {audio: 'audioinput', video: 'videoinput'};
+ return window.MediaStreamTrack.getSources(function(devices) {
+ resolve(devices.map(function(device) {
+ return {label: device.label,
+ kind: kinds[device.kind],
+ deviceId: device.id,
+ groupId: ''};
+ }));
+ });
+ });
+ },
+ getSupportedConstraints: function() {
+ return {
+ deviceId: true, echoCancellation: true, facingMode: true,
+ frameRate: true, height: true, width: true
+ };
+ }
+ };
+ }
+
+ // A shim for getUserMedia method on the mediaDevices object.
+ // TODO(KaptenJansson) remove once implemented in Chrome stable.
+ if (!navigator.mediaDevices.getUserMedia) {
+ navigator.mediaDevices.getUserMedia = function(constraints) {
+ return getUserMediaPromise_(constraints);
+ };
+ } else {
+ // Even though Chrome 45 has navigator.mediaDevices and a getUserMedia
+ // function which returns a Promise, it does not accept spec-style
+ // constraints.
+ var origGetUserMedia = navigator.mediaDevices.getUserMedia.
+ bind(navigator.mediaDevices);
+ navigator.mediaDevices.getUserMedia = function(cs) {
+ return shimConstraints_(cs, function(c) {
+ return origGetUserMedia(c).then(function(stream) {
+ if (c.audio && !stream.getAudioTracks().length ||
+ c.video && !stream.getVideoTracks().length) {
+ stream.getTracks().forEach(function(track) {
+ track.stop();
+ });
+ throw new DOMException('', 'NotFoundError');
+ }
+ return stream;
+ }, function(e) {
+ return Promise.reject(shimError_(e));
+ });
+ });
+ };
+ }
+
+ // Dummy devicechange event methods.
+ // TODO(KaptenJansson) remove once implemented in Chrome stable.
+ if (typeof navigator.mediaDevices.addEventListener === 'undefined') {
+ navigator.mediaDevices.addEventListener = function() {
+ logging('Dummy mediaDevices.addEventListener called.');
+ };
+ }
+ if (typeof navigator.mediaDevices.removeEventListener === 'undefined') {
+ navigator.mediaDevices.removeEventListener = function() {
+ logging('Dummy mediaDevices.removeEventListener called.');
+ };
+ }
+ };
+
+},{"../utils.js":12}],6:[function(require,module,exports){
+ /*
+ * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree.
+ */
+ /* eslint-env node */
+ 'use strict';
+
+ var utils = require('../utils');
+ var shimRTCPeerConnection = require('./rtcpeerconnection_shim');
+
+ module.exports = {
+ shimGetUserMedia: require('./getusermedia'),
+ shimPeerConnection: function(window) {
+ var browserDetails = utils.detectBrowser(window);
+
+ if (window.RTCIceGatherer) {
+ // ORTC defines an RTCIceCandidate object but no constructor.
+ // Not implemented in Edge.
+ if (!window.RTCIceCandidate) {
+ window.RTCIceCandidate = function(args) {
+ return args;
+ };
+ }
+ // ORTC does not have a session description object but
+ // other browsers (i.e. Chrome) that will support both PC and ORTC
+ // in the future might have this defined already.
+ if (!window.RTCSessionDescription) {
+ window.RTCSessionDescription = function(args) {
+ return args;
+ };
+ }
+ // this adds an additional event listener to MediaStrackTrack that signals
+ // when a tracks enabled property was changed. Workaround for a bug in
+ // addStream, see below. No longer required in 15025+
+ if (browserDetails.version < 15025) {
+ var origMSTEnabled = Object.getOwnPropertyDescriptor(
+ window.MediaStreamTrack.prototype, 'enabled');
+ Object.defineProperty(window.MediaStreamTrack.prototype, 'enabled', {
+ set: function(value) {
+ origMSTEnabled.set.call(this, value);
+ var ev = new Event('enabled');
+ ev.enabled = value;
+ this.dispatchEvent(ev);
+ }
+ });
+ }
+ }
+
+ // ORTC defines the DTMF sender a bit different.
+ // https://github.com/w3c/ortc/issues/714
+ if (window.RTCRtpSender && !('dtmf' in window.RTCRtpSender.prototype)) {
+ Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', {
+ get: function() {
+ if (this._dtmf === undefined) {
+ if (this.track.kind === 'audio') {
+ this._dtmf = new window.RTCDtmfSender(this);
+ } else if (this.track.kind === 'video') {
+ this._dtmf = null;
+ }
+ }
+ return this._dtmf;
+ }
+ });
+ }
+
+ window.RTCPeerConnection =
+ shimRTCPeerConnection(window, browserDetails.version);
+ },
+ shimReplaceTrack: function(window) {
+ // ORTC has replaceTrack -- https://github.com/w3c/ortc/issues/614
+ if (window.RTCRtpSender &&
+ !('replaceTrack' in window.RTCRtpSender.prototype)) {
+ window.RTCRtpSender.prototype.replaceTrack =
+ window.RTCRtpSender.prototype.setTrack;
+ }
+ }
+ };
+
+},{"../utils":12,"./getusermedia":7,"./rtcpeerconnection_shim":8}],7:[function(require,module,exports){
+ /*
+ * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree.
+ */
+ /* eslint-env node */
+ 'use strict';
+
+// Expose public methods.
+ module.exports = function(window) {
+ var navigator = window && window.navigator;
+
+ var shimError_ = function(e) {
+ return {
+ name: {PermissionDeniedError: 'NotAllowedError'}[e.name] || e.name,
+ message: e.message,
+ constraint: e.constraint,
+ toString: function() {
+ return this.name;
+ }
+ };
+ };
+
+ // getUserMedia error shim.
+ var origGetUserMedia = navigator.mediaDevices.getUserMedia.
+ bind(navigator.mediaDevices);
+ navigator.mediaDevices.getUserMedia = function(c) {
+ return origGetUserMedia(c).catch(function(e) {
+ return Promise.reject(shimError_(e));
+ });
+ };
+ };
+
+},{}],8:[function(require,module,exports){
+ /*
+ * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree.
+ */
+ /* eslint-env node */
+ 'use strict';
+
+ var SDPUtils = require('sdp');
+
+// sort tracks such that they follow an a-v-a-v...
+// pattern.
+ function sortTracks(tracks) {
+ var audioTracks = tracks.filter(function(track) {
+ return track.kind === 'audio';
+ });
+ var videoTracks = tracks.filter(function(track) {
+ return track.kind === 'video';
+ });
+ tracks = [];
+ while (audioTracks.length || videoTracks.length) {
+ if (audioTracks.length) {
+ tracks.push(audioTracks.shift());
+ }
+ if (videoTracks.length) {
+ tracks.push(videoTracks.shift());
+ }
+ }
+ return tracks;
+ }
+
+// Edge does not like
+// 1) stun:
+// 2) turn: that does not have all of turn:host:port?transport=udp
+// 3) turn: with ipv6 addresses
+// 4) turn: occurring muliple times
+ function filterIceServers(iceServers, edgeVersion) {
+ var hasTurn = false;
+ iceServers = JSON.parse(JSON.stringify(iceServers));
+ return iceServers.filter(function(server) {
+ if (server && (server.urls || server.url)) {
+ var urls = server.urls || server.url;
+ if (server.url && !server.urls) {
+ console.warn('RTCIceServer.url is deprecated! Use urls instead.');
+ }
+ var isString = typeof urls === 'string';
+ if (isString) {
+ urls = [urls];
+ }
+ urls = urls.filter(function(url) {
+ var validTurn = url.indexOf('turn:') === 0 &&
+ url.indexOf('transport=udp') !== -1 &&
+ url.indexOf('turn:[') === -1 &&
+ !hasTurn;
+
+ if (validTurn) {
+ hasTurn = true;
+ return true;
+ }
+ return url.indexOf('stun:') === 0 && edgeVersion >= 14393;
+ });
+
+ delete server.url;
+ server.urls = isString ? urls[0] : urls;
+ return !!urls.length;
+ }
+ return false;
+ });
+ }
+
+// Determines the intersection of local and remote capabilities.
+ function getCommonCapabilities(localCapabilities, remoteCapabilities) {
+ var commonCapabilities = {
+ codecs: [],
+ headerExtensions: [],
+ fecMechanisms: []
+ };
+
+ var findCodecByPayloadType = function(pt, codecs) {
+ pt = parseInt(pt, 10);
+ for (var i = 0; i < codecs.length; i++) {
+ if (codecs[i].payloadType === pt ||
+ codecs[i].preferredPayloadType === pt) {
+ return codecs[i];
+ }
+ }
+ };
+
+ var rtxCapabilityMatches = function(lRtx, rRtx, lCodecs, rCodecs) {
+ var lCodec = findCodecByPayloadType(lRtx.parameters.apt, lCodecs);
+ var rCodec = findCodecByPayloadType(rRtx.parameters.apt, rCodecs);
+ return lCodec && rCodec &&
+ lCodec.name.toLowerCase() === rCodec.name.toLowerCase();
+ };
+
+ localCapabilities.codecs.forEach(function(lCodec) {
+ for (var i = 0; i < remoteCapabilities.codecs.length; i++) {
+ var rCodec = remoteCapabilities.codecs[i];
+ if (lCodec.name.toLowerCase() === rCodec.name.toLowerCase() &&
+ lCodec.clockRate === rCodec.clockRate) {
+ if (lCodec.name.toLowerCase() === 'rtx' &&
+ lCodec.parameters && rCodec.parameters.apt) {
+ // for RTX we need to find the local rtx that has a apt
+ // which points to the same local codec as the remote one.
+ if (!rtxCapabilityMatches(lCodec, rCodec,
+ localCapabilities.codecs, remoteCapabilities.codecs)) {
+ continue;
+ }
+ }
+ rCodec = JSON.parse(JSON.stringify(rCodec)); // deepcopy
+ // number of channels is the highest common number of channels
+ rCodec.numChannels = Math.min(lCodec.numChannels,
+ rCodec.numChannels);
+ // push rCodec so we reply with offerer payload type
+ commonCapabilities.codecs.push(rCodec);
+
+ // determine common feedback mechanisms
+ rCodec.rtcpFeedback = rCodec.rtcpFeedback.filter(function(fb) {
+ for (var j = 0; j < lCodec.rtcpFeedback.length; j++) {
+ if (lCodec.rtcpFeedback[j].type === fb.type &&
+ lCodec.rtcpFeedback[j].parameter === fb.parameter) {
+ return true;
+ }
+ }
+ return false;
+ });
+ // FIXME: also need to determine .parameters
+ // see https://github.com/openpeer/ortc/issues/569
+ break;
+ }
+ }
+ });
+
+ localCapabilities.headerExtensions.forEach(function(lHeaderExtension) {
+ for (var i = 0; i < remoteCapabilities.headerExtensions.length;
+ i++) {
+ var rHeaderExtension = remoteCapabilities.headerExtensions[i];
+ if (lHeaderExtension.uri === rHeaderExtension.uri) {
+ commonCapabilities.headerExtensions.push(rHeaderExtension);
+ break;
+ }
+ }
+ });
+
+ // FIXME: fecMechanisms
+ return commonCapabilities;
+ }
+
+// is action=setLocalDescription with type allowed in signalingState
+ function isActionAllowedInSignalingState(action, type, signalingState) {
+ return {
+ offer: {
+ setLocalDescription: ['stable', 'have-local-offer'],
+ setRemoteDescription: ['stable', 'have-remote-offer']
+ },
+ answer: {
+ setLocalDescription: ['have-remote-offer', 'have-local-pranswer'],
+ setRemoteDescription: ['have-local-offer', 'have-remote-pranswer']
+ }
+ }[type][action].indexOf(signalingState) !== -1;
+ }
+
+ module.exports = function(window, edgeVersion) {
+ var RTCPeerConnection = function(config) {
+ var self = this;
+
+ var _eventTarget = document.createDocumentFragment();
+ ['addEventListener', 'removeEventListener', 'dispatchEvent']
+ .forEach(function(method) {
+ self[method] = _eventTarget[method].bind(_eventTarget);
+ });
+
+ this.needNegotiation = false;
+
+ this.onicecandidate = null;
+ this.onaddstream = null;
+ this.ontrack = null;
+ this.onremovestream = null;
+ this.onsignalingstatechange = null;
+ this.oniceconnectionstatechange = null;
+ this.onicegatheringstatechange = null;
+ this.onnegotiationneeded = null;
+ this.ondatachannel = null;
+ this.canTrickleIceCandidates = null;
+
+ this.localStreams = [];
+ this.remoteStreams = [];
+ this.getLocalStreams = function() {
+ return self.localStreams;
+ };
+ this.getRemoteStreams = function() {
+ return self.remoteStreams;
+ };
+
+ this.localDescription = new window.RTCSessionDescription({
+ type: '',
+ sdp: ''
+ });
+ this.remoteDescription = new window.RTCSessionDescription({
+ type: '',
+ sdp: ''
+ });
+ this.signalingState = 'stable';
+ this.iceConnectionState = 'new';
+ this.iceGatheringState = 'new';
+
+ this.iceOptions = {
+ gatherPolicy: 'all',
+ iceServers: []
+ };
+ if (config && config.iceTransportPolicy) {
+ switch (config.iceTransportPolicy) {
+ case 'all':
+ case 'relay':
+ this.iceOptions.gatherPolicy = config.iceTransportPolicy;
+ break;
+ default:
+ // don't set iceTransportPolicy.
+ break;
+ }
+ }
+ this.usingBundle = config && config.bundlePolicy === 'max-bundle';
+
+ if (config && config.iceServers) {
+ this.iceOptions.iceServers = filterIceServers(config.iceServers,
+ edgeVersion);
+ }
+ this._config = config || {};
+
+ // per-track iceGathers, iceTransports, dtlsTransports, rtpSenders, ...
+ // everything that is needed to describe a SDP m-line.
+ this.transceivers = [];
+
+ // since the iceGatherer is currently created in createOffer but we
+ // must not emit candidates until after setLocalDescription we buffer
+ // them in this array.
+ this._localIceCandidatesBuffer = [];
+
+ this._sdpSessionId = SDPUtils.generateSessionId();
+ };
+
+ RTCPeerConnection.prototype._emitGatheringStateChange = function() {
+ var event = new Event('icegatheringstatechange');
+ this.dispatchEvent(event);
+ if (this.onicegatheringstatechange !== null) {
+ this.onicegatheringstatechange(event);
+ }
+ };
+
+ RTCPeerConnection.prototype._emitBufferedCandidates = function() {
+ var self = this;
+ var sections = SDPUtils.splitSections(self.localDescription.sdp);
+ // FIXME: need to apply ice candidates in a way which is async but
+ // in-order
+ this._localIceCandidatesBuffer.forEach(function(event) {
+ var end = !event.candidate || Object.keys(event.candidate).length === 0;
+ if (end) {
+ for (var j = 1; j < sections.length; j++) {
+ if (sections[j].indexOf('\r\na=end-of-candidates\r\n') === -1) {
+ sections[j] += 'a=end-of-candidates\r\n';
+ }
+ }
+ } else {
+ sections[event.candidate.sdpMLineIndex + 1] +=
+ 'a=' + event.candidate.candidate + '\r\n';
+ }
+ self.localDescription.sdp = sections.join('');
+ self.dispatchEvent(event);
+ if (self.onicecandidate !== null) {
+ self.onicecandidate(event);
+ }
+ if (!event.candidate && self.iceGatheringState !== 'complete') {
+ var complete = self.transceivers.every(function(transceiver) {
+ return transceiver.iceGatherer &&
+ transceiver.iceGatherer.state === 'completed';
+ });
+ if (complete && self.iceGatheringStateChange !== 'complete') {
+ self.iceGatheringState = 'complete';
+ self._emitGatheringStateChange();
+ }
+ }
+ });
+ this._localIceCandidatesBuffer = [];
+ };
+
+ RTCPeerConnection.prototype.getConfiguration = function() {
+ return this._config;
+ };
+
+ // internal helper to create a transceiver object.
+ // (whih is not yet the same as the WebRTC 1.0 transceiver)
+ RTCPeerConnection.prototype._createTransceiver = function(kind) {
+ var hasBundleTransport = this.transceivers.length > 0;
+ var transceiver = {
+ track: null,
+ iceGatherer: null,
+ iceTransport: null,
+ dtlsTransport: null,
+ localCapabilities: null,
+ remoteCapabilities: null,
+ rtpSender: null,
+ rtpReceiver: null,
+ kind: kind,
+ mid: null,
+ sendEncodingParameters: null,
+ recvEncodingParameters: null,
+ stream: null,
+ wantReceive: true
+ };
+ if (this.usingBundle && hasBundleTransport) {
+ transceiver.iceTransport = this.transceivers[0].iceTransport;
+ transceiver.dtlsTransport = this.transceivers[0].dtlsTransport;
+ } else {
+ var transports = this._createIceAndDtlsTransports();
+ transceiver.iceTransport = transports.iceTransport;
+ transceiver.dtlsTransport = transports.dtlsTransport;
+ }
+ this.transceivers.push(transceiver);
+ return transceiver;
+ };
+
+ RTCPeerConnection.prototype.addTrack = function(track, stream) {
+ var transceiver;
+ for (var i = 0; i < this.transceivers.length; i++) {
+ if (!this.transceivers[i].track &&
+ this.transceivers[i].kind === track.kind) {
+ transceiver = this.transceivers[i];
+ }
+ }
+ if (!transceiver) {
+ transceiver = this._createTransceiver(track.kind);
+ }
+
+ transceiver.track = track;
+ transceiver.stream = stream;
+ transceiver.rtpSender = new window.RTCRtpSender(track,
+ transceiver.dtlsTransport);
+
+ this._maybeFireNegotiationNeeded();
+ return transceiver.rtpSender;
+ };
+
+ RTCPeerConnection.prototype.addStream = function(stream) {
+ var self = this;
+ if (edgeVersion >= 15025) {
+ this.localStreams.push(stream);
+ stream.getTracks().forEach(function(track) {
+ self.addTrack(track, stream);
+ });
+ } else {
+ // Clone is necessary for local demos mostly, attaching directly
+ // to two different senders does not work (build 10547).
+ // Fixed in 15025 (or earlier)
+ var clonedStream = stream.clone();
+ stream.getTracks().forEach(function(track, idx) {
+ var clonedTrack = clonedStream.getTracks()[idx];
+ track.addEventListener('enabled', function(event) {
+ clonedTrack.enabled = event.enabled;
+ });
+ });
+ clonedStream.getTracks().forEach(function(track) {
+ self.addTrack(track, clonedStream);
+ });
+ this.localStreams.push(clonedStream);
+ }
+ this._maybeFireNegotiationNeeded();
+ };
+
+ RTCPeerConnection.prototype.removeStream = function(stream) {
+ var idx = this.localStreams.indexOf(stream);
+ if (idx > -1) {
+ this.localStreams.splice(idx, 1);
+ this._maybeFireNegotiationNeeded();
+ }
+ };
+
+ RTCPeerConnection.prototype.getSenders = function() {
+ return this.transceivers.filter(function(transceiver) {
+ return !!transceiver.rtpSender;
+ })
+ .map(function(transceiver) {
+ return transceiver.rtpSender;
+ });
+ };
+
+ RTCPeerConnection.prototype.getReceivers = function() {
+ return this.transceivers.filter(function(transceiver) {
+ return !!transceiver.rtpReceiver;
+ })
+ .map(function(transceiver) {
+ return transceiver.rtpReceiver;
+ });
+ };
+
+ // Create ICE gatherer and hook it up.
+ RTCPeerConnection.prototype._createIceGatherer = function(mid,
+ sdpMLineIndex) {
+ var self = this;
+ var iceGatherer = new window.RTCIceGatherer(self.iceOptions);
+ iceGatherer.onlocalcandidate = function(evt) {
+ var event = new Event('icecandidate');
+ event.candidate = {sdpMid: mid, sdpMLineIndex: sdpMLineIndex};
+
+ var cand = evt.candidate;
+ var end = !cand || Object.keys(cand).length === 0;
+ // Edge emits an empty object for RTCIceCandidateComplete‥
+ if (end) {
+ // polyfill since RTCIceGatherer.state is not implemented in
+ // Edge 10547 yet.
+ if (iceGatherer.state === undefined) {
+ iceGatherer.state = 'completed';
+ }
+ } else {
+ // RTCIceCandidate doesn't have a component, needs to be added
+ cand.component = 1;
+ event.candidate.candidate = SDPUtils.writeCandidate(cand);
+ }
+
+ // update local description.
+ var sections = SDPUtils.splitSections(self.localDescription.sdp);
+ if (!end) {
+ sections[event.candidate.sdpMLineIndex + 1] +=
+ 'a=' + event.candidate.candidate + '\r\n';
+ } else {
+ sections[event.candidate.sdpMLineIndex + 1] +=
+ 'a=end-of-candidates\r\n';
+ }
+ self.localDescription.sdp = sections.join('');
+ var transceivers = self._pendingOffer ? self._pendingOffer :
+ self.transceivers;
+ var complete = transceivers.every(function(transceiver) {
+ return transceiver.iceGatherer &&
+ transceiver.iceGatherer.state === 'completed';
+ });
+
+ // Emit candidate if localDescription is set.
+ // Also emits null candidate when all gatherers are complete.
+ switch (self.iceGatheringState) {
+ case 'new':
+ if (!end) {
+ self._localIceCandidatesBuffer.push(event);
+ }
+ if (end && complete) {
+ self._localIceCandidatesBuffer.push(
+ new Event('icecandidate'));
+ }
+ break;
+ case 'gathering':
+ self._emitBufferedCandidates();
+ if (!end) {
+ self.dispatchEvent(event);
+ if (self.onicecandidate !== null) {
+ self.onicecandidate(event);
+ }
+ }
+ if (complete) {
+ self.dispatchEvent(new Event('icecandidate'));
+ if (self.onicecandidate !== null) {
+ self.onicecandidate(new Event('icecandidate'));
+ }
+ self.iceGatheringState = 'complete';
+ self._emitGatheringStateChange();
+ }
+ break;
+ case 'complete':
+ // should not happen... currently!
+ break;
+ default: // no-op.
+ break;
+ }
+ };
+ return iceGatherer;
+ };
+
+ // Create ICE transport and DTLS transport.
+ RTCPeerConnection.prototype._createIceAndDtlsTransports = function() {
+ var self = this;
+ var iceTransport = new window.RTCIceTransport(null);
+ iceTransport.onicestatechange = function() {
+ self._updateConnectionState();
+ };
+
+ var dtlsTransport = new window.RTCDtlsTransport(iceTransport);
+ dtlsTransport.ondtlsstatechange = function() {
+ self._updateConnectionState();
+ };
+ dtlsTransport.onerror = function() {
+ // onerror does not set state to failed by itself.
+ Object.defineProperty(dtlsTransport, 'state',
+ {value: 'failed', writable: true});
+ self._updateConnectionState();
+ };
+
+ return {
+ iceTransport: iceTransport,
+ dtlsTransport: dtlsTransport
+ };
+ };
+
+ // Destroy ICE gatherer, ICE transport and DTLS transport.
+ // Without triggering the callbacks.
+ RTCPeerConnection.prototype._disposeIceAndDtlsTransports = function(
+ sdpMLineIndex) {
+ var iceGatherer = this.transceivers[sdpMLineIndex].iceGatherer;
+ if (iceGatherer) {
+ delete iceGatherer.onlocalcandidate;
+ delete this.transceivers[sdpMLineIndex].iceGatherer;
+ }
+ var iceTransport = this.transceivers[sdpMLineIndex].iceTransport;
+ if (iceTransport) {
+ delete iceTransport.onicestatechange;
+ delete this.transceivers[sdpMLineIndex].iceTransport;
+ }
+ var dtlsTransport = this.transceivers[sdpMLineIndex].dtlsTransport;
+ if (dtlsTransport) {
+ delete dtlsTransport.ondtlsstatechange;
+ delete dtlsTransport.onerror;
+ delete this.transceivers[sdpMLineIndex].dtlsTransport;
+ }
+ };
+
+ // Start the RTP Sender and Receiver for a transceiver.
+ RTCPeerConnection.prototype._transceive = function(transceiver,
+ send, recv) {
+ var params = getCommonCapabilities(transceiver.localCapabilities,
+ transceiver.remoteCapabilities);
+ if (send && transceiver.rtpSender) {
+ params.encodings = transceiver.sendEncodingParameters;
+ params.rtcp = {
+ cname: SDPUtils.localCName,
+ compound: transceiver.rtcpParameters.compound
+ };
+ if (transceiver.recvEncodingParameters.length) {
+ params.rtcp.ssrc = transceiver.recvEncodingParameters[0].ssrc;
+ }
+ transceiver.rtpSender.send(params);
+ }
+ if (recv && transceiver.rtpReceiver) {
+ // remove RTX field in Edge 14942
+ if (transceiver.kind === 'video'
+ && transceiver.recvEncodingParameters
+ && edgeVersion < 15019) {
+ transceiver.recvEncodingParameters.forEach(function(p) {
+ delete p.rtx;
+ });
+ }
+ params.encodings = transceiver.recvEncodingParameters;
+ params.rtcp = {
+ cname: transceiver.rtcpParameters.cname,
+ compound: transceiver.rtcpParameters.compound
+ };
+ if (transceiver.sendEncodingParameters.length) {
+ params.rtcp.ssrc = transceiver.sendEncodingParameters[0].ssrc;
+ }
+ transceiver.rtpReceiver.receive(params);
+ }
+ };
+
+ RTCPeerConnection.prototype.setLocalDescription = function(description) {
+ var self = this;
+
+ if (!isActionAllowedInSignalingState('setLocalDescription',
+ description.type, this.signalingState)) {
+ var e = new Error('Can not set local ' + description.type +
+ ' in state ' + this.signalingState);
+ e.name = 'InvalidStateError';
+ if (arguments.length > 2 && typeof arguments[2] === 'function') {
+ window.setTimeout(arguments[2], 0, e);
+ }
+ return Promise.reject(e);
+ }
+
+ var sections;
+ var sessionpart;
+ if (description.type === 'offer') {
+ // FIXME: What was the purpose of this empty if statement?
+ // if (!this._pendingOffer) {
+ // } else {
+ if (this._pendingOffer) {
+ // VERY limited support for SDP munging. Limited to:
+ // * changing the order of codecs
+ sections = SDPUtils.splitSections(description.sdp);
+ sessionpart = sections.shift();
+ sections.forEach(function(mediaSection, sdpMLineIndex) {
+ var caps = SDPUtils.parseRtpParameters(mediaSection);
+ self._pendingOffer[sdpMLineIndex].localCapabilities = caps;
+ });
+ this.transceivers = this._pendingOffer;
+ delete this._pendingOffer;
+ }
+ } else if (description.type === 'answer') {
+ sections = SDPUtils.splitSections(self.remoteDescription.sdp);
+ sessionpart = sections.shift();
+ var isIceLite = SDPUtils.matchPrefix(sessionpart,
+ 'a=ice-lite').length > 0;
+ sections.forEach(function(mediaSection, sdpMLineIndex) {
+ var transceiver = self.transceivers[sdpMLineIndex];
+ var iceGatherer = transceiver.iceGatherer;
+ var iceTransport = transceiver.iceTransport;
+ var dtlsTransport = transceiver.dtlsTransport;
+ var localCapabilities = transceiver.localCapabilities;
+ var remoteCapabilities = transceiver.remoteCapabilities;
+
+ var rejected = SDPUtils.isRejected(mediaSection);
+
+ if (!rejected && !transceiver.isDatachannel) {
+ var remoteIceParameters = SDPUtils.getIceParameters(
+ mediaSection, sessionpart);
+ var remoteDtlsParameters = SDPUtils.getDtlsParameters(
+ mediaSection, sessionpart);
+ if (isIceLite) {
+ remoteDtlsParameters.role = 'server';
+ }
+
+ if (!self.usingBundle || sdpMLineIndex === 0) {
+ iceTransport.start(iceGatherer, remoteIceParameters,
+ isIceLite ? 'controlling' : 'controlled');
+ dtlsTransport.start(remoteDtlsParameters);
+ }
+
+ // Calculate intersection of capabilities.
+ var params = getCommonCapabilities(localCapabilities,
+ remoteCapabilities);
+
+ // Start the RTCRtpSender. The RTCRtpReceiver for this
+ // transceiver has already been started in setRemoteDescription.
+ self._transceive(transceiver,
+ params.codecs.length > 0,
+ false);
+ }
+ });
+ }
+
+ this.localDescription = {
+ type: description.type,
+ sdp: description.sdp
+ };
+ switch (description.type) {
+ case 'offer':
+ this._updateSignalingState('have-local-offer');
+ break;
+ case 'answer':
+ this._updateSignalingState('stable');
+ break;
+ default:
+ throw new TypeError('unsupported type "' + description.type +
+ '"');
+ }
+
+ // If a success callback was provided, emit ICE candidates after it
+ // has been executed. Otherwise, emit callback after the Promise is
+ // resolved.
+ var hasCallback = arguments.length > 1 &&
+ typeof arguments[1] === 'function';
+ if (hasCallback) {
+ var cb = arguments[1];
+ window.setTimeout(function() {
+ cb();
+ if (self.iceGatheringState === 'new') {
+ self.iceGatheringState = 'gathering';
+ self._emitGatheringStateChange();
+ }
+ self._emitBufferedCandidates();
+ }, 0);
+ }
+ var p = Promise.resolve();
+ p.then(function() {
+ if (!hasCallback) {
+ if (self.iceGatheringState === 'new') {
+ self.iceGatheringState = 'gathering';
+ self._emitGatheringStateChange();
+ }
+ // Usually candidates will be emitted earlier.
+ window.setTimeout(self._emitBufferedCandidates.bind(self), 500);
+ }
+ });
+ return p;
+ };
+
+ RTCPeerConnection.prototype.setRemoteDescription = function(description) {
+ var self = this;
+
+ if (!isActionAllowedInSignalingState('setRemoteDescription',
+ description.type, this.signalingState)) {
+ var e = new Error('Can not set remote ' + description.type +
+ ' in state ' + this.signalingState);
+ e.name = 'InvalidStateError';
+ if (arguments.length > 2 && typeof arguments[2] === 'function') {
+ window.setTimeout(arguments[2], 0, e);
+ }
+ return Promise.reject(e);
+ }
+
+ var streams = {};
+ var receiverList = [];
+ var sections = SDPUtils.splitSections(description.sdp);
+ var sessionpart = sections.shift();
+ var isIceLite = SDPUtils.matchPrefix(sessionpart,
+ 'a=ice-lite').length > 0;
+ var usingBundle = SDPUtils.matchPrefix(sessionpart,
+ 'a=group:BUNDLE ').length > 0;
+ this.usingBundle = usingBundle;
+ var iceOptions = SDPUtils.matchPrefix(sessionpart,
+ 'a=ice-options:')[0];
+ if (iceOptions) {
+ this.canTrickleIceCandidates = iceOptions.substr(14).split(' ')
+ .indexOf('trickle') >= 0;
+ } else {
+ this.canTrickleIceCandidates = false;
+ }
+
+ sections.forEach(function(mediaSection, sdpMLineIndex) {
+ var lines = SDPUtils.splitLines(mediaSection);
+ var kind = SDPUtils.getKind(mediaSection);
+ var rejected = SDPUtils.isRejected(mediaSection);
+ var protocol = lines[0].substr(2).split(' ')[2];
+
+ var direction = SDPUtils.getDirection(mediaSection, sessionpart);
+ var remoteMsid = SDPUtils.parseMsid(mediaSection);
+
+ var mid = SDPUtils.getMid(mediaSection) || SDPUtils.generateIdentifier();
+
+ // Reject datachannels which are not implemented yet.
+ if (kind === 'application' && protocol === 'DTLS/SCTP') {
+ self.transceivers[sdpMLineIndex] = {
+ mid: mid,
+ isDatachannel: true
+ };
+ return;
+ }
+
+ var transceiver;
+ var iceGatherer;
+ var iceTransport;
+ var dtlsTransport;
+ var rtpReceiver;
+ var sendEncodingParameters;
+ var recvEncodingParameters;
+ var localCapabilities;
+
+ var track;
+ // FIXME: ensure the mediaSection has rtcp-mux set.
+ var remoteCapabilities = SDPUtils.parseRtpParameters(mediaSection);
+ var remoteIceParameters;
+ var remoteDtlsParameters;
+ if (!rejected) {
+ remoteIceParameters = SDPUtils.getIceParameters(mediaSection,
+ sessionpart);
+ remoteDtlsParameters = SDPUtils.getDtlsParameters(mediaSection,
+ sessionpart);
+ remoteDtlsParameters.role = 'client';
+ }
+ recvEncodingParameters =
+ SDPUtils.parseRtpEncodingParameters(mediaSection);
+
+ var rtcpParameters = SDPUtils.parseRtcpParameters(mediaSection);
+
+ var isComplete = SDPUtils.matchPrefix(mediaSection,
+ 'a=end-of-candidates', sessionpart).length > 0;
+ var cands = SDPUtils.matchPrefix(mediaSection, 'a=candidate:')
+ .map(function(cand) {
+ return SDPUtils.parseCandidate(cand);
+ })
+ .filter(function(cand) {
+ return cand.component === '1' || cand.component === 1;
+ });
+
+ // Check if we can use BUNDLE and dispose transports.
+ if ((description.type === 'offer' || description.type === 'answer') &&
+ !rejected && usingBundle && sdpMLineIndex > 0 &&
+ self.transceivers[sdpMLineIndex]) {
+ self._disposeIceAndDtlsTransports(sdpMLineIndex);
+ self.transceivers[sdpMLineIndex].iceGatherer =
+ self.transceivers[0].iceGatherer;
+ self.transceivers[sdpMLineIndex].iceTransport =
+ self.transceivers[0].iceTransport;
+ self.transceivers[sdpMLineIndex].dtlsTransport =
+ self.transceivers[0].dtlsTransport;
+ if (self.transceivers[sdpMLineIndex].rtpSender) {
+ self.transceivers[sdpMLineIndex].rtpSender.setTransport(
+ self.transceivers[0].dtlsTransport);
+ }
+ if (self.transceivers[sdpMLineIndex].rtpReceiver) {
+ self.transceivers[sdpMLineIndex].rtpReceiver.setTransport(
+ self.transceivers[0].dtlsTransport);
+ }
+ }
+ if (description.type === 'offer' && !rejected) {
+ transceiver = self.transceivers[sdpMLineIndex] ||
+ self._createTransceiver(kind);
+ transceiver.mid = mid;
+
+ if (!transceiver.iceGatherer) {
+ transceiver.iceGatherer = usingBundle && sdpMLineIndex > 0 ?
+ self.transceivers[0].iceGatherer :
+ self._createIceGatherer(mid, sdpMLineIndex);
+ }
+
+ if (isComplete && cands.length &&
+ (!usingBundle || sdpMLineIndex === 0)) {
+ transceiver.iceTransport.setRemoteCandidates(cands);
+ }
+
+ localCapabilities = window.RTCRtpReceiver.getCapabilities(kind);
+
+ // filter RTX until additional stuff needed for RTX is implemented
+ // in adapter.js
+ if (edgeVersion < 15019) {
+ localCapabilities.codecs = localCapabilities.codecs.filter(
+ function(codec) {
+ return codec.name !== 'rtx';
+ });
+ }
+
+ sendEncodingParameters = [{
+ ssrc: (2 * sdpMLineIndex + 2) * 1001
+ }];
+
+ if (direction === 'sendrecv' || direction === 'sendonly') {
+ rtpReceiver = new window.RTCRtpReceiver(transceiver.dtlsTransport,
+ kind);
+
+ track = rtpReceiver.track;
+ // FIXME: does not work with Plan B.
+ if (remoteMsid) {
+ if (!streams[remoteMsid.stream]) {
+ streams[remoteMsid.stream] = new window.MediaStream();
+ Object.defineProperty(streams[remoteMsid.stream], 'id', {
+ get: function() {
+ return remoteMsid.stream;
+ }
+ });
+ }
+ Object.defineProperty(track, 'id', {
+ get: function() {
+ return remoteMsid.track;
+ }
+ });
+ streams[remoteMsid.stream].addTrack(track);
+ receiverList.push([track, rtpReceiver,
+ streams[remoteMsid.stream]]);
+ } else {
+ if (!streams.default) {
+ streams.default = new window.MediaStream();
+ }
+ streams.default.addTrack(track);
+ receiverList.push([track, rtpReceiver, streams.default]);
+ }
+ }
+
+ transceiver.localCapabilities = localCapabilities;
+ transceiver.remoteCapabilities = remoteCapabilities;
+ transceiver.rtpReceiver = rtpReceiver;
+ transceiver.rtcpParameters = rtcpParameters;
+ transceiver.sendEncodingParameters = sendEncodingParameters;
+ transceiver.recvEncodingParameters = recvEncodingParameters;
+
+ // Start the RTCRtpReceiver now. The RTPSender is started in
+ // setLocalDescription.
+ self._transceive(self.transceivers[sdpMLineIndex],
+ false,
+ direction === 'sendrecv' || direction === 'sendonly');
+ } else if (description.type === 'answer' && !rejected) {
+ transceiver = self.transceivers[sdpMLineIndex];
+ iceGatherer = transceiver.iceGatherer;
+ iceTransport = transceiver.iceTransport;
+ dtlsTransport = transceiver.dtlsTransport;
+ rtpReceiver = transceiver.rtpReceiver;
+ sendEncodingParameters = transceiver.sendEncodingParameters;
+ localCapabilities = transceiver.localCapabilities;
+
+ self.transceivers[sdpMLineIndex].recvEncodingParameters =
+ recvEncodingParameters;
+ self.transceivers[sdpMLineIndex].remoteCapabilities =
+ remoteCapabilities;
+ self.transceivers[sdpMLineIndex].rtcpParameters = rtcpParameters;
+
+ if (!usingBundle || sdpMLineIndex === 0) {
+ if ((isIceLite || isComplete) && cands.length) {
+ iceTransport.setRemoteCandidates(cands);
+ }
+ iceTransport.start(iceGatherer, remoteIceParameters,
+ 'controlling');
+ dtlsTransport.start(remoteDtlsParameters);
+ }
+
+ self._transceive(transceiver,
+ direction === 'sendrecv' || direction === 'recvonly',
+ direction === 'sendrecv' || direction === 'sendonly');
+
+ if (rtpReceiver &&
+ (direction === 'sendrecv' || direction === 'sendonly')) {
+ track = rtpReceiver.track;
+ if (remoteMsid) {
+ if (!streams[remoteMsid.stream]) {
+ streams[remoteMsid.stream] = new window.MediaStream();
+ }
+ streams[remoteMsid.stream].addTrack(track);
+ receiverList.push([track, rtpReceiver, streams[remoteMsid.stream]]);
+ } else {
+ if (!streams.default) {
+ streams.default = new window.MediaStream();
+ }
+ streams.default.addTrack(track);
+ receiverList.push([track, rtpReceiver, streams.default]);
+ }
+ } else {
+ // FIXME: actually the receiver should be created later.
+ delete transceiver.rtpReceiver;
+ }
+ }
+ });
+
+ this.remoteDescription = {
+ type: description.type,
+ sdp: description.sdp
+ };
+ switch (description.type) {
+ case 'offer':
+ this._updateSignalingState('have-remote-offer');
+ break;
+ case 'answer':
+ this._updateSignalingState('stable');
+ break;
+ default:
+ throw new TypeError('unsupported type "' + description.type +
+ '"');
+ }
+ Object.keys(streams).forEach(function(sid) {
+ var stream = streams[sid];
+ if (stream.getTracks().length) {
+ self.remoteStreams.push(stream);
+ var event = new Event('addstream');
+ event.stream = stream;
+ self.dispatchEvent(event);
+ if (self.onaddstream !== null) {
+ window.setTimeout(function() {
+ self.onaddstream(event);
+ }, 0);
+ }
+
+ receiverList.forEach(function(item) {
+ var track = item[0];
+ var receiver = item[1];
+ if (stream.id !== item[2].id) {
+ return;
+ }
+ var trackEvent = new Event('track');
+ trackEvent.track = track;
+ trackEvent.receiver = receiver;
+ trackEvent.streams = [stream];
+ self.dispatchEvent(trackEvent);
+ if (self.ontrack !== null) {
+ window.setTimeout(function() {
+ self.ontrack(trackEvent);
+ }, 0);
+ }
+ });
+ }
+ });
+
+ // check whether addIceCandidate({}) was called within four seconds after
+ // setRemoteDescription.
+ window.setTimeout(function() {
+ if (!(self && self.transceivers)) {
+ return;
+ }
+ self.transceivers.forEach(function(transceiver) {
+ if (transceiver.iceTransport &&
+ transceiver.iceTransport.state === 'new' &&
+ transceiver.iceTransport.getRemoteCandidates().length > 0) {
+ console.warn('Timeout for addRemoteCandidate. Consider sending ' +
+ 'an end-of-candidates notification');
+ transceiver.iceTransport.addRemoteCandidate({});
+ }
+ });
+ }, 4000);
+
+ if (arguments.length > 1 && typeof arguments[1] === 'function') {
+ window.setTimeout(arguments[1], 0);
+ }
+ return Promise.resolve();
+ };
+
+ RTCPeerConnection.prototype.close = function() {
+ this.transceivers.forEach(function(transceiver) {
+ /* not yet
+ if (transceiver.iceGatherer) {
+ transceiver.iceGatherer.close();
+ }
+ */
+ if (transceiver.iceTransport) {
+ transceiver.iceTransport.stop();
+ }
+ if (transceiver.dtlsTransport) {
+ transceiver.dtlsTransport.stop();
+ }
+ if (transceiver.rtpSender) {
+ transceiver.rtpSender.stop();
+ }
+ if (transceiver.rtpReceiver) {
+ transceiver.rtpReceiver.stop();
+ }
+ });
+ // FIXME: clean up tracks, local streams, remote streams, etc
+ this._updateSignalingState('closed');
+ };
+
+ // Update the signaling state.
+ RTCPeerConnection.prototype._updateSignalingState = function(newState) {
+ this.signalingState = newState;
+ var event = new Event('signalingstatechange');
+ this.dispatchEvent(event);
+ if (this.onsignalingstatechange !== null) {
+ this.onsignalingstatechange(event);
+ }
+ };
+
+ // Determine whether to fire the negotiationneeded event.
+ RTCPeerConnection.prototype._maybeFireNegotiationNeeded = function() {
+ var self = this;
+ if (this.signalingState !== 'stable' || this.needNegotiation === true) {
+ return;
+ }
+ this.needNegotiation = true;
+ window.setTimeout(function() {
+ if (self.needNegotiation === false) {
+ return;
+ }
+ self.needNegotiation = false;
+ var event = new Event('negotiationneeded');
+ self.dispatchEvent(event);
+ if (self.onnegotiationneeded !== null) {
+ self.onnegotiationneeded(event);
+ }
+ }, 0);
+ };
+
+ // Update the connection state.
+ RTCPeerConnection.prototype._updateConnectionState = function() {
+ var self = this;
+ var newState;
+ var states = {
+ 'new': 0,
+ closed: 0,
+ connecting: 0,
+ checking: 0,
+ connected: 0,
+ completed: 0,
+ disconnected: 0,
+ failed: 0
+ };
+ this.transceivers.forEach(function(transceiver) {
+ states[transceiver.iceTransport.state]++;
+ states[transceiver.dtlsTransport.state]++;
+ });
+ // ICETransport.completed and connected are the same for this purpose.
+ states.connected += states.completed;
+
+ newState = 'new';
+ if (states.failed > 0) {
+ newState = 'failed';
+ } else if (states.connecting > 0 || states.checking > 0) {
+ newState = 'connecting';
+ } else if (states.disconnected > 0) {
+ newState = 'disconnected';
+ } else if (states.new > 0) {
+ newState = 'new';
+ } else if (states.connected > 0 || states.completed > 0) {
+ newState = 'connected';
+ }
+
+ if (newState !== self.iceConnectionState) {
+ self.iceConnectionState = newState;
+ var event = new Event('iceconnectionstatechange');
+ this.dispatchEvent(event);
+ if (this.oniceconnectionstatechange !== null) {
+ this.oniceconnectionstatechange(event);
+ }
+ }
+ };
+
+ RTCPeerConnection.prototype.createOffer = function() {
+ var self = this;
+ if (this._pendingOffer) {
+ throw new Error('createOffer called while there is a pending offer.');
+ }
+ var offerOptions;
+ if (arguments.length === 1 && typeof arguments[0] !== 'function') {
+ offerOptions = arguments[0];
+ } else if (arguments.length === 3) {
+ offerOptions = arguments[2];
+ }
+
+ var numAudioTracks = this.transceivers.filter(function(t) {
+ return t.kind === 'audio';
+ }).length;
+ var numVideoTracks = this.transceivers.filter(function(t) {
+ return t.kind === 'video';
+ }).length;
+
+ // Determine number of audio and video tracks we need to send/recv.
+ if (offerOptions) {
+ // Reject Chrome legacy constraints.
+ if (offerOptions.mandatory || offerOptions.optional) {
+ throw new TypeError(
+ 'Legacy mandatory/optional constraints not supported.');
+ }
+ if (offerOptions.offerToReceiveAudio !== undefined) {
+ if (offerOptions.offerToReceiveAudio === true) {
+ numAudioTracks = 1;
+ } else if (offerOptions.offerToReceiveAudio === false) {
+ numAudioTracks = 0;
+ } else {
+ numAudioTracks = offerOptions.offerToReceiveAudio;
+ }
+ }
+ if (offerOptions.offerToReceiveVideo !== undefined) {
+ if (offerOptions.offerToReceiveVideo === true) {
+ numVideoTracks = 1;
+ } else if (offerOptions.offerToReceiveVideo === false) {
+ numVideoTracks = 0;
+ } else {
+ numVideoTracks = offerOptions.offerToReceiveVideo;
+ }
+ }
+ }
+
+ this.transceivers.forEach(function(transceiver) {
+ if (transceiver.kind === 'audio') {
+ numAudioTracks--;
+ if (numAudioTracks < 0) {
+ transceiver.wantReceive = false;
+ }
+ } else if (transceiver.kind === 'video') {
+ numVideoTracks--;
+ if (numVideoTracks < 0) {
+ transceiver.wantReceive = false;
+ }
+ }
+ });
+
+ // Create M-lines for recvonly streams.
+ while (numAudioTracks > 0 || numVideoTracks > 0) {
+ if (numAudioTracks > 0) {
+ this._createTransceiver('audio');
+ numAudioTracks--;
+ }
+ if (numVideoTracks > 0) {
+ this._createTransceiver('video');
+ numVideoTracks--;
+ }
+ }
+ // reorder tracks
+ var transceivers = sortTracks(this.transceivers);
+
+ var sdp = SDPUtils.writeSessionBoilerplate(this._sdpSessionId);
+ transceivers.forEach(function(transceiver, sdpMLineIndex) {
+ // For each track, create an ice gatherer, ice transport,
+ // dtls transport, potentially rtpsender and rtpreceiver.
+ var track = transceiver.track;
+ var kind = transceiver.kind;
+ var mid = SDPUtils.generateIdentifier();
+ transceiver.mid = mid;
+
+ if (!transceiver.iceGatherer) {
+ transceiver.iceGatherer = self.usingBundle && sdpMLineIndex > 0 ?
+ transceivers[0].iceGatherer :
+ self._createIceGatherer(mid, sdpMLineIndex);
+ }
+
+ var localCapabilities = window.RTCRtpSender.getCapabilities(kind);
+ // filter RTX until additional stuff needed for RTX is implemented
+ // in adapter.js
+ if (edgeVersion < 15019) {
+ localCapabilities.codecs = localCapabilities.codecs.filter(
+ function(codec) {
+ return codec.name !== 'rtx';
+ });
+ }
+ localCapabilities.codecs.forEach(function(codec) {
+ // work around https://bugs.chromium.org/p/webrtc/issues/detail?id=6552
+ // by adding level-asymmetry-allowed=1
+ if (codec.name === 'H264' &&
+ codec.parameters['level-asymmetry-allowed'] === undefined) {
+ codec.parameters['level-asymmetry-allowed'] = '1';
+ }
+ });
+
+ // generate an ssrc now, to be used later in rtpSender.send
+ var sendEncodingParameters = [{
+ ssrc: (2 * sdpMLineIndex + 1) * 1001
+ }];
+ if (track) {
+ // add RTX
+ if (edgeVersion >= 15019 && kind === 'video') {
+ sendEncodingParameters[0].rtx = {
+ ssrc: (2 * sdpMLineIndex + 1) * 1001 + 1
+ };
+ }
+ }
+
+ if (transceiver.wantReceive) {
+ transceiver.rtpReceiver = new window.RTCRtpReceiver(
+ transceiver.dtlsTransport,
+ kind
+ );
+ }
+
+ transceiver.localCapabilities = localCapabilities;
+ transceiver.sendEncodingParameters = sendEncodingParameters;
+ });
+
+ // always offer BUNDLE and dispose on return if not supported.
+ if (this._config.bundlePolicy !== 'max-compat') {
+ sdp += 'a=group:BUNDLE ' + transceivers.map(function(t) {
+ return t.mid;
+ }).join(' ') + '\r\n';
+ }
+ sdp += 'a=ice-options:trickle\r\n';
+
+ transceivers.forEach(function(transceiver, sdpMLineIndex) {
+ sdp += SDPUtils.writeMediaSection(transceiver,
+ transceiver.localCapabilities, 'offer', transceiver.stream);
+ sdp += 'a=rtcp-rsize\r\n';
+ });
+
+ this._pendingOffer = transceivers;
+ var desc = new window.RTCSessionDescription({
+ type: 'offer',
+ sdp: sdp
+ });
+ if (arguments.length && typeof arguments[0] === 'function') {
+ window.setTimeout(arguments[0], 0, desc);
+ }
+ return Promise.resolve(desc);
+ };
+
+ RTCPeerConnection.prototype.createAnswer = function() {
+ var sdp = SDPUtils.writeSessionBoilerplate(this._sdpSessionId);
+ if (this.usingBundle) {
+ sdp += 'a=group:BUNDLE ' + this.transceivers.map(function(t) {
+ return t.mid;
+ }).join(' ') + '\r\n';
+ }
+ this.transceivers.forEach(function(transceiver, sdpMLineIndex) {
+ if (transceiver.isDatachannel) {
+ sdp += 'm=application 0 DTLS/SCTP 5000\r\n' +
+ 'c=IN IP4 0.0.0.0\r\n' +
+ 'a=mid:' + transceiver.mid + '\r\n';
+ return;
+ }
+
+ // FIXME: look at direction.
+ if (transceiver.stream) {
+ var localTrack;
+ if (transceiver.kind === 'audio') {
+ localTrack = transceiver.stream.getAudioTracks()[0];
+ } else if (transceiver.kind === 'video') {
+ localTrack = transceiver.stream.getVideoTracks()[0];
+ }
+ if (localTrack) {
+ // add RTX
+ if (edgeVersion >= 15019 && transceiver.kind === 'video') {
+ transceiver.sendEncodingParameters[0].rtx = {
+ ssrc: (2 * sdpMLineIndex + 2) * 1001 + 1
+ };
+ }
+ }
+ }
+
+ // Calculate intersection of capabilities.
+ var commonCapabilities = getCommonCapabilities(
+ transceiver.localCapabilities,
+ transceiver.remoteCapabilities);
+
+ var hasRtx = commonCapabilities.codecs.filter(function(c) {
+ return c.name.toLowerCase() === 'rtx';
+ }).length;
+ if (!hasRtx && transceiver.sendEncodingParameters[0].rtx) {
+ delete transceiver.sendEncodingParameters[0].rtx;
+ }
+
+ sdp += SDPUtils.writeMediaSection(transceiver, commonCapabilities,
+ 'answer', transceiver.stream);
+ if (transceiver.rtcpParameters &&
+ transceiver.rtcpParameters.reducedSize) {
+ sdp += 'a=rtcp-rsize\r\n';
+ }
+ });
+
+ var desc = new window.RTCSessionDescription({
+ type: 'answer',
+ sdp: sdp
+ });
+ if (arguments.length && typeof arguments[0] === 'function') {
+ window.setTimeout(arguments[0], 0, desc);
+ }
+ return Promise.resolve(desc);
+ };
+
+ RTCPeerConnection.prototype.addIceCandidate = function(candidate) {
+ if (!candidate) {
+ for (var j = 0; j < this.transceivers.length; j++) {
+ this.transceivers[j].iceTransport.addRemoteCandidate({});
+ if (this.usingBundle) {
+ return Promise.resolve();
+ }
+ }
+ } else {
+ var mLineIndex = candidate.sdpMLineIndex;
+ if (candidate.sdpMid) {
+ for (var i = 0; i < this.transceivers.length; i++) {
+ if (this.transceivers[i].mid === candidate.sdpMid) {
+ mLineIndex = i;
+ break;
+ }
+ }
+ }
+ var transceiver = this.transceivers[mLineIndex];
+ if (transceiver) {
+ var cand = Object.keys(candidate.candidate).length > 0 ?
+ SDPUtils.parseCandidate(candidate.candidate) : {};
+ // Ignore Chrome's invalid candidates since Edge does not like them.
+ if (cand.protocol === 'tcp' && (cand.port === 0 || cand.port === 9)) {
+ return Promise.resolve();
+ }
+ // Ignore RTCP candidates, we assume RTCP-MUX.
+ if (cand.component &&
+ !(cand.component === '1' || cand.component === 1)) {
+ return Promise.resolve();
+ }
+ transceiver.iceTransport.addRemoteCandidate(cand);
+
+ // update the remoteDescription.
+ var sections = SDPUtils.splitSections(this.remoteDescription.sdp);
+ sections[mLineIndex + 1] += (cand.type ? candidate.candidate.trim()
+ : 'a=end-of-candidates') + '\r\n';
+ this.remoteDescription.sdp = sections.join('');
+ }
+ }
+ if (arguments.length > 1 && typeof arguments[1] === 'function') {
+ window.setTimeout(arguments[1], 0);
+ }
+ return Promise.resolve();
+ };
+
+ RTCPeerConnection.prototype.getStats = function() {
+ var promises = [];
+ this.transceivers.forEach(function(transceiver) {
+ ['rtpSender', 'rtpReceiver', 'iceGatherer', 'iceTransport',
+ 'dtlsTransport'].forEach(function(method) {
+ if (transceiver[method]) {
+ promises.push(transceiver[method].getStats());
+ }
+ });
+ });
+ var cb = arguments.length > 1 && typeof arguments[1] === 'function' &&
+ arguments[1];
+ var fixStatsType = function(stat) {
+ return {
+ inboundrtp: 'inbound-rtp',
+ outboundrtp: 'outbound-rtp',
+ candidatepair: 'candidate-pair',
+ localcandidate: 'local-candidate',
+ remotecandidate: 'remote-candidate'
+ }[stat.type] || stat.type;
+ };
+ return new Promise(function(resolve) {
+ // shim getStats with maplike support
+ var results = new Map();
+ Promise.all(promises).then(function(res) {
+ res.forEach(function(result) {
+ Object.keys(result).forEach(function(id) {
+ result[id].type = fixStatsType(result[id]);
+ results.set(id, result[id]);
+ });
+ });
+ if (cb) {
+ window.setTimeout(cb, 0, results);
+ }
+ resolve(results);
+ });
+ });
+ };
+ return RTCPeerConnection;
+ };
+
+},{"sdp":1}],9:[function(require,module,exports){
+ /*
+ * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree.
+ */
+ /* eslint-env node */
+ 'use strict';
+
+ var utils = require('../utils');
+
+ var firefoxShim = {
+ shimOnTrack: function(window) {
+ if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in
+ window.RTCPeerConnection.prototype)) {
+ Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', {
+ get: function() {
+ return this._ontrack;
+ },
+ set: function(f) {
+ if (this._ontrack) {
+ this.removeEventListener('track', this._ontrack);
+ this.removeEventListener('addstream', this._ontrackpoly);
+ }
+ this.addEventListener('track', this._ontrack = f);
+ this.addEventListener('addstream', this._ontrackpoly = function(e) {
+ e.stream.getTracks().forEach(function(track) {
+ var event = new Event('track');
+ event.track = track;
+ event.receiver = {track: track};
+ event.streams = [e.stream];
+ this.dispatchEvent(event);
+ }.bind(this));
+ }.bind(this));
+ }
+ });
+ }
+ },
+
+ shimSourceObject: function(window) {
+ // Firefox has supported mozSrcObject since FF22, unprefixed in 42.
+ if (typeof window === 'object') {
+ if (window.HTMLMediaElement &&
+ !('srcObject' in window.HTMLMediaElement.prototype)) {
+ // Shim the srcObject property, once, when HTMLMediaElement is found.
+ Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', {
+ get: function() {
+ return this.mozSrcObject;
+ },
+ set: function(stream) {
+ this.mozSrcObject = stream;
+ }
+ });
+ }
+ }
+ },
+
+ shimPeerConnection: function(window) {
+ var browserDetails = utils.detectBrowser(window);
+
+ if (typeof window !== 'object' || !(window.RTCPeerConnection ||
+ window.mozRTCPeerConnection)) {
+ return; // probably media.peerconnection.enabled=false in about:config
+ }
+ // The RTCPeerConnection object.
+ if (!window.RTCPeerConnection) {
+ window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+ if (browserDetails.version < 38) {
+ // .urls is not supported in FF < 38.
+ // create RTCIceServers with a single url.
+ if (pcConfig && pcConfig.iceServers) {
+ var newIceServers = [];
+ for (var i = 0; i < pcConfig.iceServers.length; i++) {
+ var server = pcConfig.iceServers[i];
+ if (server.hasOwnProperty('urls')) {
+ for (var j = 0; j < server.urls.length; j++) {
+ var newServer = {
+ url: server.urls[j]
+ };
+ if (server.urls[j].indexOf('turn') === 0) {
+ newServer.username = server.username;
+ newServer.credential = server.credential;
+ }
+ newIceServers.push(newServer);
+ }
+ } else {
+ newIceServers.push(pcConfig.iceServers[i]);
+ }
+ }
+ pcConfig.iceServers = newIceServers;
+ }
+ }
+ return new window.mozRTCPeerConnection(pcConfig, pcConstraints);
+ };
+ window.RTCPeerConnection.prototype =
+ window.mozRTCPeerConnection.prototype;
+
+ // wrap static methods. Currently just generateCertificate.
+ if (window.mozRTCPeerConnection.generateCertificate) {
+ Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
+ get: function() {
+ return window.mozRTCPeerConnection.generateCertificate;
+ }
+ });
+ }
+
+ window.RTCSessionDescription = window.mozRTCSessionDescription;
+ window.RTCIceCandidate = window.mozRTCIceCandidate;
+ }
+
+ // shim away need for obsolete RTCIceCandidate/RTCSessionDescription.
+ ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']
+ .forEach(function(method) {
+ var nativeMethod = window.RTCPeerConnection.prototype[method];
+ window.RTCPeerConnection.prototype[method] = function() {
+ arguments[0] = new ((method === 'addIceCandidate') ?
+ window.RTCIceCandidate :
+ window.RTCSessionDescription)(arguments[0]);
+ return nativeMethod.apply(this, arguments);
+ };
+ });
+
+ // support for addIceCandidate(null or undefined)
+ var nativeAddIceCandidate =
+ window.RTCPeerConnection.prototype.addIceCandidate;
+ window.RTCPeerConnection.prototype.addIceCandidate = function() {
+ if (!arguments[0]) {
+ if (arguments[1]) {
+ arguments[1].apply(null);
+ }
+ return Promise.resolve();
+ }
+ return nativeAddIceCandidate.apply(this, arguments);
+ };
+
+ // shim getStats with maplike support
+ var makeMapStats = function(stats) {
+ var map = new Map();
+ Object.keys(stats).forEach(function(key) {
+ map.set(key, stats[key]);
+ map[key] = stats[key];
+ });
+ return map;
+ };
+
+ var modernStatsTypes = {
+ inboundrtp: 'inbound-rtp',
+ outboundrtp: 'outbound-rtp',
+ candidatepair: 'candidate-pair',
+ localcandidate: 'local-candidate',
+ remotecandidate: 'remote-candidate'
+ };
+
+ var nativeGetStats = window.RTCPeerConnection.prototype.getStats;
+ window.RTCPeerConnection.prototype.getStats = function(
+ selector,
+ onSucc,
+ onErr
+ ) {
+ return nativeGetStats.apply(this, [selector || null])
+ .then(function(stats) {
+ if (browserDetails.version < 48) {
+ stats = makeMapStats(stats);
+ }
+ if (browserDetails.version < 53 && !onSucc) {
+ // Shim only promise getStats with spec-hyphens in type names
+ // Leave callback version alone; misc old uses of forEach before Map
+ try {
+ stats.forEach(function(stat) {
+ stat.type = modernStatsTypes[stat.type] || stat.type;
+ });
+ } catch (e) {
+ if (e.name !== 'TypeError') {
+ throw e;
+ }
+ // Avoid TypeError: "type" is read-only, in old versions. 34-43ish
+ stats.forEach(function(stat, i) {
+ stats.set(i, Object.assign({}, stat, {
+ type: modernStatsTypes[stat.type] || stat.type
+ }));
+ });
+ }
+ }
+ return stats;
+ })
+ .then(onSucc, onErr);
+ };
+ }
+ };
+
+// Expose public methods.
+ module.exports = {
+ shimOnTrack: firefoxShim.shimOnTrack,
+ shimSourceObject: firefoxShim.shimSourceObject,
+ shimPeerConnection: firefoxShim.shimPeerConnection,
+ shimGetUserMedia: require('./getusermedia')
+ };
+
+},{"../utils":12,"./getusermedia":10}],10:[function(require,module,exports){
+ /*
+ * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree.
+ */
+ /* eslint-env node */
+ 'use strict';
+
+ var utils = require('../utils');
+ var logging = utils.log;
+
+// Expose public methods.
+ module.exports = function(window) {
+ var browserDetails = utils.detectBrowser(window);
+ var navigator = window && window.navigator;
+ var MediaStreamTrack = window && window.MediaStreamTrack;
+
+ var shimError_ = function(e) {
+ return {
+ name: {
+ InternalError: 'NotReadableError',
+ NotSupportedError: 'TypeError',
+ PermissionDeniedError: 'NotAllowedError',
+ SecurityError: 'NotAllowedError'
+ }[e.name] || e.name,
+ message: {
+ 'The operation is insecure.': 'The request is not allowed by the ' +
+ 'user agent or the platform in the current context.'
+ }[e.message] || e.message,
+ constraint: e.constraint,
+ toString: function() {
+ return this.name + (this.message && ': ') + this.message;
+ }
+ };
+ };
+
+ // getUserMedia constraints shim.
+ var getUserMedia_ = function(constraints, onSuccess, onError) {
+ var constraintsToFF37_ = function(c) {
+ if (typeof c !== 'object' || c.require) {
+ return c;
+ }
+ var require = [];
+ Object.keys(c).forEach(function(key) {
+ if (key === 'require' || key === 'advanced' || key === 'mediaSource') {
+ return;
+ }
+ var r = c[key] = (typeof c[key] === 'object') ?
+ c[key] : {ideal: c[key]};
+ if (r.min !== undefined ||
+ r.max !== undefined || r.exact !== undefined) {
+ require.push(key);
+ }
+ if (r.exact !== undefined) {
+ if (typeof r.exact === 'number') {
+ r. min = r.max = r.exact;
+ } else {
+ c[key] = r.exact;
+ }
+ delete r.exact;
+ }
+ if (r.ideal !== undefined) {
+ c.advanced = c.advanced || [];
+ var oc = {};
+ if (typeof r.ideal === 'number') {
+ oc[key] = {min: r.ideal, max: r.ideal};
+ } else {
+ oc[key] = r.ideal;
+ }
+ c.advanced.push(oc);
+ delete r.ideal;
+ if (!Object.keys(r).length) {
+ delete c[key];
+ }
+ }
+ });
+ if (require.length) {
+ c.require = require;
+ }
+ return c;
+ };
+ constraints = JSON.parse(JSON.stringify(constraints));
+ if (browserDetails.version < 38) {
+ logging('spec: ' + JSON.stringify(constraints));
+ if (constraints.audio) {
+ constraints.audio = constraintsToFF37_(constraints.audio);
+ }
+ if (constraints.video) {
+ constraints.video = constraintsToFF37_(constraints.video);
+ }
+ logging('ff37: ' + JSON.stringify(constraints));
+ }
+ return navigator.mozGetUserMedia(constraints, onSuccess, function(e) {
+ onError(shimError_(e));
+ });
+ };
+
+ // Returns the result of getUserMedia as a Promise.
+ var getUserMediaPromise_ = function(constraints) {
+ return new Promise(function(resolve, reject) {
+ getUserMedia_(constraints, resolve, reject);
+ });
+ };
+
+ // Shim for mediaDevices on older versions.
+ if (!navigator.mediaDevices) {
+ navigator.mediaDevices = {getUserMedia: getUserMediaPromise_,
+ addEventListener: function() { },
+ removeEventListener: function() { }
+ };
+ }
+ navigator.mediaDevices.enumerateDevices =
+ navigator.mediaDevices.enumerateDevices || function() {
+ return new Promise(function(resolve) {
+ var infos = [
+ {kind: 'audioinput', deviceId: 'default', label: '', groupId: ''},
+ {kind: 'videoinput', deviceId: 'default', label: '', groupId: ''}
+ ];
+ resolve(infos);
+ });
+ };
+
+ if (browserDetails.version < 41) {
+ // Work around http://bugzil.la/1169665
+ var orgEnumerateDevices =
+ navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices);
+ navigator.mediaDevices.enumerateDevices = function() {
+ return orgEnumerateDevices().then(undefined, function(e) {
+ if (e.name === 'NotFoundError') {
+ return [];
+ }
+ throw e;
+ });
+ };
+ }
+ if (browserDetails.version < 49) {
+ var origGetUserMedia = navigator.mediaDevices.getUserMedia.
+ bind(navigator.mediaDevices);
+ navigator.mediaDevices.getUserMedia = function(c) {
+ return origGetUserMedia(c).then(function(stream) {
+ // Work around https://bugzil.la/802326
+ if (c.audio && !stream.getAudioTracks().length ||
+ c.video && !stream.getVideoTracks().length) {
+ stream.getTracks().forEach(function(track) {
+ track.stop();
+ });
+ throw new DOMException('The object can not be found here.',
+ 'NotFoundError');
+ }
+ return stream;
+ }, function(e) {
+ return Promise.reject(shimError_(e));
+ });
+ };
+ }
+ if (!(browserDetails.version > 55 &&
+ 'autoGainControl' in navigator.mediaDevices.getSupportedConstraints())) {
+ var remap = function(obj, a, b) {
+ if (a in obj && !(b in obj)) {
+ obj[b] = obj[a];
+ delete obj[a];
+ }
+ };
+
+ var nativeGetUserMedia = navigator.mediaDevices.getUserMedia.
+ bind(navigator.mediaDevices);
+ navigator.mediaDevices.getUserMedia = function(c) {
+ if (typeof c === 'object' && typeof c.audio === 'object') {
+ c = JSON.parse(JSON.stringify(c));
+ remap(c.audio, 'autoGainControl', 'mozAutoGainControl');
+ remap(c.audio, 'noiseSuppression', 'mozNoiseSuppression');
+ }
+ return nativeGetUserMedia(c);
+ };
+
+ if (MediaStreamTrack && MediaStreamTrack.prototype.getSettings) {
+ var nativeGetSettings = MediaStreamTrack.prototype.getSettings;
+ MediaStreamTrack.prototype.getSettings = function() {
+ var obj = nativeGetSettings.apply(this, arguments);
+ remap(obj, 'mozAutoGainControl', 'autoGainControl');
+ remap(obj, 'mozNoiseSuppression', 'noiseSuppression');
+ return obj;
+ };
+ }
+
+ if (MediaStreamTrack && MediaStreamTrack.prototype.applyConstraints) {
+ var nativeApplyConstraints = MediaStreamTrack.prototype.applyConstraints;
+ MediaStreamTrack.prototype.applyConstraints = function(c) {
+ if (this.kind === 'audio' && typeof c === 'object') {
+ c = JSON.parse(JSON.stringify(c));
+ remap(c, 'autoGainControl', 'mozAutoGainControl');
+ remap(c, 'noiseSuppression', 'mozNoiseSuppression');
+ }
+ return nativeApplyConstraints.apply(this, [c]);
+ };
+ }
+ }
+ navigator.getUserMedia = function(constraints, onSuccess, onError) {
+ if (browserDetails.version < 44) {
+ return getUserMedia_(constraints, onSuccess, onError);
+ }
+ // Replace Firefox 44+'s deprecation warning with unprefixed version.
+ utils.deprecated('navigator.getUserMedia',
+ 'navigator.mediaDevices.getUserMedia');
+ navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError);
+ };
+ };
+
+},{"../utils":12}],11:[function(require,module,exports){
+ /*
+ * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree.
+ */
+ 'use strict';
+ var utils = require('../utils');
+
+ var safariShim = {
+ // TODO: DrAlex, should be here, double check against LayoutTests
+
+ // TODO: once the back-end for the mac port is done, add.
+ // TODO: check for webkitGTK+
+ // shimPeerConnection: function() { },
+
+ shimLocalStreamsAPI: function(window) {
+ if (typeof window !== 'object' || !window.RTCPeerConnection) {
+ return;
+ }
+ if (!('getLocalStreams' in window.RTCPeerConnection.prototype)) {
+ window.RTCPeerConnection.prototype.getLocalStreams = function() {
+ if (!this._localStreams) {
+ this._localStreams = [];
+ }
+ return this._localStreams;
+ };
+ }
+ if (!('getStreamById' in window.RTCPeerConnection.prototype)) {
+ window.RTCPeerConnection.prototype.getStreamById = function(id) {
+ var result = null;
+ if (this._localStreams) {
+ this._localStreams.forEach(function(stream) {
+ if (stream.id === id) {
+ result = stream;
+ }
+ });
+ }
+ if (this._remoteStreams) {
+ this._remoteStreams.forEach(function(stream) {
+ if (stream.id === id) {
+ result = stream;
+ }
+ });
+ }
+ return result;
+ };
+ }
+ if (!('addStream' in window.RTCPeerConnection.prototype)) {
+ var _addTrack = window.RTCPeerConnection.prototype.addTrack;
+ window.RTCPeerConnection.prototype.addStream = function(stream) {
+ if (!this._localStreams) {
+ this._localStreams = [];
+ }
+ if (this._localStreams.indexOf(stream) === -1) {
+ this._localStreams.push(stream);
+ }
+ var self = this;
+ stream.getTracks().forEach(function(track) {
+ _addTrack.call(self, track, stream);
+ });
+ };
+
+ window.RTCPeerConnection.prototype.addTrack = function(track, stream) {
+ if (stream) {
+ if (!this._localStreams) {
+ this._localStreams = [stream];
+ } else if (this._localStreams.indexOf(stream) === -1) {
+ this._localStreams.push(stream);
+ }
+ }
+ _addTrack.call(this, track, stream);
+ };
+ }
+ if (!('removeStream' in window.RTCPeerConnection.prototype)) {
+ window.RTCPeerConnection.prototype.removeStream = function(stream) {
+ if (!this._localStreams) {
+ this._localStreams = [];
+ }
+ var index = this._localStreams.indexOf(stream);
+ if (index === -1) {
+ return;
+ }
+ this._localStreams.splice(index, 1);
+ var self = this;
+ var tracks = stream.getTracks();
+ this.getSenders().forEach(function(sender) {
+ if (tracks.indexOf(sender.track) !== -1) {
+ self.removeTrack(sender);
+ }
+ });
+ };
+ }
+ },
+ shimRemoteStreamsAPI: function(window) {
+ if (typeof window !== 'object' || !window.RTCPeerConnection) {
+ return;
+ }
+ if (!('getRemoteStreams' in window.RTCPeerConnection.prototype)) {
+ window.RTCPeerConnection.prototype.getRemoteStreams = function() {
+ return this._remoteStreams ? this._remoteStreams : [];
+ };
+ }
+ if (!('onaddstream' in window.RTCPeerConnection.prototype)) {
+ Object.defineProperty(window.RTCPeerConnection.prototype, 'onaddstream', {
+ get: function() {
+ return this._onaddstream;
+ },
+ set: function(f) {
+ if (this._onaddstream) {
+ this.removeEventListener('addstream', this._onaddstream);
+ this.removeEventListener('track', this._onaddstreampoly);
+ }
+ this.addEventListener('addstream', this._onaddstream = f);
+ this.addEventListener('track', this._onaddstreampoly = function(e) {
+ var stream = e.streams[0];
+ if (!this._remoteStreams) {
+ this._remoteStreams = [];
+ }
+ if (this._remoteStreams.indexOf(stream) >= 0) {
+ return;
+ }
+ this._remoteStreams.push(stream);
+ var event = new Event('addstream');
+ event.stream = e.streams[0];
+ this.dispatchEvent(event);
+ }.bind(this));
+ }
+ });
+ }
+ },
+ shimCallbacksAPI: function(window) {
+ if (typeof window !== 'object' || !window.RTCPeerConnection) {
+ return;
+ }
+ var prototype = window.RTCPeerConnection.prototype;
+ var createOffer = prototype.createOffer;
+ var createAnswer = prototype.createAnswer;
+ var setLocalDescription = prototype.setLocalDescription;
+ var setRemoteDescription = prototype.setRemoteDescription;
+ var addIceCandidate = prototype.addIceCandidate;
+
+ prototype.createOffer = function(successCallback, failureCallback) {
+ var options = (arguments.length >= 2) ? arguments[2] : arguments[0];
+ var promise = createOffer.apply(this, [options]);
+ if (!failureCallback) {
+ return promise;
+ }
+ promise.then(successCallback, failureCallback);
+ return Promise.resolve();
+ };
+
+ prototype.createAnswer = function(successCallback, failureCallback) {
+ var options = (arguments.length >= 2) ? arguments[2] : arguments[0];
+ var promise = createAnswer.apply(this, [options]);
+ if (!failureCallback) {
+ return promise;
+ }
+ promise.then(successCallback, failureCallback);
+ return Promise.resolve();
+ };
+
+ var withCallback = function(description, successCallback, failureCallback) {
+ var promise = setLocalDescription.apply(this, [description]);
+ if (!failureCallback) {
+ return promise;
+ }
+ promise.then(successCallback, failureCallback);
+ return Promise.resolve();
+ };
+ prototype.setLocalDescription = withCallback;
+
+ withCallback = function(description, successCallback, failureCallback) {
+ var promise = setRemoteDescription.apply(this, [description]);
+ if (!failureCallback) {
+ return promise;
+ }
+ promise.then(successCallback, failureCallback);
+ return Promise.resolve();
+ };
+ prototype.setRemoteDescription = withCallback;
+
+ withCallback = function(candidate, successCallback, failureCallback) {
+ var promise = addIceCandidate.apply(this, [candidate]);
+ if (!failureCallback) {
+ return promise;
+ }
+ promise.then(successCallback, failureCallback);
+ return Promise.resolve();
+ };
+ prototype.addIceCandidate = withCallback;
+ },
+ shimGetUserMedia: function(window) {
+ var navigator = window && window.navigator;
+
+ if (!navigator.getUserMedia) {
+ if (navigator.webkitGetUserMedia) {
+ navigator.getUserMedia = navigator.webkitGetUserMedia.bind(navigator);
+ } else if (navigator.mediaDevices &&
+ navigator.mediaDevices.getUserMedia) {
+ navigator.getUserMedia = function(constraints, cb, errcb) {
+ navigator.mediaDevices.getUserMedia(constraints)
+ .then(cb, errcb);
+ }.bind(navigator);
+ }
+ }
+ },
+ shimRTCIceServerUrls: function(window) {
+ // migrate from non-spec RTCIceServer.url to RTCIceServer.urls
+ var OrigPeerConnection = window.RTCPeerConnection;
+ window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+ if (pcConfig && pcConfig.iceServers) {
+ var newIceServers = [];
+ for (var i = 0; i < pcConfig.iceServers.length; i++) {
+ var server = pcConfig.iceServers[i];
+ if (!server.hasOwnProperty('urls') &&
+ server.hasOwnProperty('url')) {
+ utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls');
+ server = JSON.parse(JSON.stringify(server));
+ server.urls = server.url;
+ delete server.url;
+ newIceServers.push(server);
+ } else {
+ newIceServers.push(pcConfig.iceServers[i]);
+ }
+ }
+ pcConfig.iceServers = newIceServers;
+ }
+ return new OrigPeerConnection(pcConfig, pcConstraints);
+ };
+ window.RTCPeerConnection.prototype = OrigPeerConnection.prototype;
+ // wrap static methods. Currently just generateCertificate.
+ Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
+ get: function() {
+ return OrigPeerConnection.generateCertificate;
+ }
+ });
+ }
+ };
+
+// Expose public methods.
+ module.exports = {
+ shimCallbacksAPI: safariShim.shimCallbacksAPI,
+ shimLocalStreamsAPI: safariShim.shimLocalStreamsAPI,
+ shimRemoteStreamsAPI: safariShim.shimRemoteStreamsAPI,
+ shimGetUserMedia: safariShim.shimGetUserMedia,
+ shimRTCIceServerUrls: safariShim.shimRTCIceServerUrls
+ // TODO
+ // shimPeerConnection: safariShim.shimPeerConnection
+ };
+
+},{"../utils":12}],12:[function(require,module,exports){
+ /*
+ * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree.
+ */
+ /* eslint-env node */
+ 'use strict';
+
+ var logDisabled_ = true;
+ var deprecationWarnings_ = true;
+
+// Utility methods.
+ var utils = {
+ disableLog: function(bool) {
+ if (typeof bool !== 'boolean') {
+ return new Error('Argument type: ' + typeof bool +
+ '. Please use a boolean.');
+ }
+ logDisabled_ = bool;
+ return (bool) ? 'adapter.js logging disabled' :
+ 'adapter.js logging enabled';
+ },
+
+ /**
+ * Disable or enable deprecation warnings
+ * @param {!boolean} bool set to true to disable warnings.
+ */
+ disableWarnings: function(bool) {
+ if (typeof bool !== 'boolean') {
+ return new Error('Argument type: ' + typeof bool +
+ '. Please use a boolean.');
+ }
+ deprecationWarnings_ = !bool;
+ return 'adapter.js deprecation warnings ' + (bool ? 'disabled' : 'enabled');
+ },
+
+ log: function() {
+ if (typeof window === 'object') {
+ if (logDisabled_) {
+ return;
+ }
+ if (typeof console !== 'undefined' && typeof console.log === 'function') {
+ console.log.apply(console, arguments);
+ }
+ }
+ },
+
+ /**
+ * Shows a deprecation warning suggesting the modern and spec-compatible API.
+ */
+ deprecated: function(oldMethod, newMethod) {
+ if (!deprecationWarnings_) {
+ return;
+ }
+ console.warn(oldMethod + ' is deprecated, please use ' + newMethod +
+ ' instead.');
+ },
+
+ /**
+ * Extract browser version out of the provided user agent string.
+ *
+ * @param {!string} uastring userAgent string.
+ * @param {!string} expr Regular expression used as match criteria.
+ * @param {!number} pos position in the version string to be returned.
+ * @return {!number} browser version.
+ */
+ extractVersion: function(uastring, expr, pos) {
+ var match = uastring.match(expr);
+ return match && match.length >= pos && parseInt(match[pos], 10);
+ },
+
+ /**
+ * Browser detector.
+ *
+ * @return {object} result containing browser and version
+ * properties.
+ */
+ detectBrowser: function(window) {
+ var navigator = window && window.navigator;
+
+ // Returned result object.
+ var result = {};
+ result.browser = null;
+ result.version = null;
+
+ // Fail early if it's not a browser
+ if (typeof window === 'undefined' || !window.navigator) {
+ result.browser = 'Not a browser.';
+ return result;
+ }
+
+ // Firefox.
+ if (navigator.mozGetUserMedia) {
+ result.browser = 'firefox';
+ result.version = this.extractVersion(navigator.userAgent,
+ /Firefox\/(\d+)\./, 1);
+ } else if (navigator.webkitGetUserMedia) {
+ // Chrome, Chromium, Webview, Opera, all use the chrome shim for now
+ if (window.webkitRTCPeerConnection) {
+ result.browser = 'chrome';
+ result.version = this.extractVersion(navigator.userAgent,
+ /Chrom(e|ium)\/(\d+)\./, 2);
+ } else { // Safari (in an unpublished version) or unknown webkit-based.
+ if (navigator.userAgent.match(/Version\/(\d+).(\d+)/)) {
+ result.browser = 'safari';
+ result.version = this.extractVersion(navigator.userAgent,
+ /AppleWebKit\/(\d+)\./, 1);
+ } else { // unknown webkit-based browser.
+ result.browser = 'Unsupported webkit-based browser ' +
+ 'with GUM support but no WebRTC support.';
+ return result;
+ }
+ }
+ } else if (navigator.mediaDevices &&
+ navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)) { // Edge.
+ result.browser = 'edge';
+ result.version = this.extractVersion(navigator.userAgent,
+ /Edge\/(\d+).(\d+)$/, 2);
+ } else if (navigator.mediaDevices &&
+ navigator.userAgent.match(/AppleWebKit\/(\d+)\./)) {
+ // Safari, with webkitGetUserMedia removed.
+ result.browser = 'safari';
+ result.version = this.extractVersion(navigator.userAgent,
+ /AppleWebKit\/(\d+)\./, 1);
+ } else { // Default fallthrough: not supported.
+ result.browser = 'Not a supported browser.';
+ return result;
+ }
+
+ return result;
+ },
+
+ // shimCreateObjectURL must be called before shimSourceObject to avoid loop.
+
+ shimCreateObjectURL: function(window) {
+ var URL = window && window.URL;
+
+ if (!(typeof window === 'object' && window.HTMLMediaElement &&
+ 'srcObject' in window.HTMLMediaElement.prototype)) {
+ // Only shim CreateObjectURL using srcObject if srcObject exists.
+ return undefined;
+ }
+
+ var nativeCreateObjectURL = URL.createObjectURL.bind(URL);
+ var nativeRevokeObjectURL = URL.revokeObjectURL.bind(URL);
+ var streams = new Map(), newId = 0;
+
+ URL.createObjectURL = function(stream) {
+ if ('getTracks' in stream) {
+ var url = 'polyblob:' + (++newId);
+ streams.set(url, stream);
+ utils.deprecated('URL.createObjectURL(stream)',
+ 'elem.srcObject = stream');
+ return url;
+ }
+ return nativeCreateObjectURL(stream);
+ };
+ URL.revokeObjectURL = function(url) {
+ nativeRevokeObjectURL(url);
+ streams.delete(url);
+ };
+
+ var dsc = Object.getOwnPropertyDescriptor(window.HTMLMediaElement.prototype,
+ 'src');
+ Object.defineProperty(window.HTMLMediaElement.prototype, 'src', {
+ get: function() {
+ return dsc.get.apply(this);
+ },
+ set: function(url) {
+ this.srcObject = streams.get(url) || null;
+ return dsc.set.apply(this, [url]);
+ }
+ });
+
+ var nativeSetAttribute = window.HTMLMediaElement.prototype.setAttribute;
+ window.HTMLMediaElement.prototype.setAttribute = function() {
+ if (arguments.length === 2 &&
+ ('' + arguments[0]).toLowerCase() === 'src') {
+ this.srcObject = streams.get(arguments[1]) || null;
+ }
+ return nativeSetAttribute.apply(this, arguments);
+ };
+ }
+ };
+
+// Export.
+ module.exports = {
+ log: utils.log,
+ deprecated: utils.deprecated,
+ disableLog: utils.disableLog,
+ disableWarnings: utils.disableWarnings,
+ extractVersion: utils.extractVersion,
+ shimCreateObjectURL: utils.shimCreateObjectURL,
+ detectBrowser: utils.detectBrowser.bind(utils)
+ };
+
+},{}]},{},[2])(2)
+});
\ No newline at end of file
diff --git a/packages/elmarti_video-chat/services/call_log.js b/packages/elmarti_video-chat/services/call_log.js
new file mode 100644
index 0000000..9292067
--- /dev/null
+++ b/packages/elmarti_video-chat/services/call_log.js
@@ -0,0 +1,3 @@
+import { Meteor } from 'meteor/meteor';
+
+export default new Meteor.Collection("VideoChatCallLog");
\ No newline at end of file
diff --git a/packages/elmarti_video-chat/services/client.js b/packages/elmarti_video-chat/services/client.js
new file mode 100644
index 0000000..1ef9606
--- /dev/null
+++ b/packages/elmarti_video-chat/services/client.js
@@ -0,0 +1,220 @@
+import { Tracker } from 'meteor/tracker';
+class VideoCallServices {
+ RTCConfiguration = {};
+ constructor(){
+ Tracker.autorun(()=>{
+ this.sub = Meteor.subscribe('VideoChatPublication');
+ });
+ let callLog;
+ Meteor.connection._stream.on('message', (msg) => {
+
+ msg = JSON.parse(msg);
+ if( msg.collection === 'VideoChatCallLog'
+ && msg.msg === 'removed'){
+ this.onTerminateCall();
+ }
+ if( msg.collection === 'VideoChatCallLog'
+ && msg.msg === 'added'
+ && msg.fields.target === Meteor.userId()
+ && msg.fields.status == "NEW"){
+ callLog = msg.fields;
+ this.stream = new Meteor.Streamer(msg.id);
+ this.stream.on('video_message', (stream_data) => {
+ if( typeof stream_data == "string")
+ stream_data = JSON.parse(stream_data);
+ if( stream_data.offer ){
+ navigator.mediaDevices.getUserMedia({ audio: true, video: true }).then( stream => {
+ if(this.localVideo){
+ this.localVideo.srcObject = stream;
+ this.localVideo.muted = true;
+ this.localVideo.play();
+ }
+ this.setupPeerConnection( stream, stream_data.offer );
+ }).catch( err => {
+ this.onError(err, stream_data)
+ });
+ }
+ if( stream_data.candidate ){
+ if( typeof stream_data.candidate == "string")
+ stream_data.candidate = JSON.parse(stream_data.candidate);
+ const candidate = new RTCIceCandidate(stream_data.candidate);
+ if(this.peerConnection)
+ this.peerConnection.addIceCandidate(candidate).catch(err => {
+ this.onError(err, stream_data);
+ });
+ }
+ });
+ this.onReceivePhoneCall(callLog.caller);
+ }
+ if( msg.collection === 'VideoChatCallLog'
+ && msg.msg === 'added'
+ && msg.fields.caller === Meteor.userId()
+ && msg.fields.status === 'NEW'){
+ callLog = msg.fields;
+ }
+ if (msg.msg == 'changed'
+ && msg.collection == 'VideoChatCallLog'
+ && msg.fields != undefined){
+ const { fields } = msg;
+ if ( fields.status == 'ACCEPTED' && callLog.caller == Meteor.userId() ){
+ this.onTargetAccept();
+ navigator.mediaDevices.getUserMedia({ audio: true, video: true }).then( stream => {
+ if(this.localVideo){
+ this.localVideo.srcObject = stream;
+ this.localVideo.muted = true;
+ this.localVideo.play();
+ }
+ this.setupPeerConnection(stream);
+ }).catch( err => {
+ this.onError(err, msg)
+ });
+ }
+ }
+ });
+
+ }
+ /**5
+ * Set up the peer connection
+ * @param stream {MediaStream}
+ * @param remoteDescription {RTCPeerConnection}
+ */
+ setupPeerConnection( stream, remoteDescription ){
+ this.peerConnection = new RTCPeerConnection(this.RTCConfiguration, {"optional": [ {'googIPv6': false} ] } );
+ this.onPeerConnectionCreated();
+ this.setPeerConnectionCallbacks();
+ this.peerConnection.addStream( stream );
+ if( remoteDescription )
+ this.createTargetSession( remoteDescription );
+ else
+ this.createCallSession();
+ }
+ /**
+ * Set callback for RTCPeerConnection
+ */
+ setPeerConnectionCallbacks(){
+ this.peerConnection.onicecandidate = ( event ) => {
+ if( event.candidate ){
+ this.stream.emit( 'video_message', { candidate : JSON.stringify(event.candidate) });
+ }
+ };
+ this.peerConnection.oniceconnectionstatechange = ( event ) => {
+ console.log(event);
+ };
+ this.peerConnection.onaddstream = function( stream ) {
+ if(this.remoteVideo) {
+ this.remoteVideo.srcObject = stream.stream;
+ if(this.remoteVideo.paused)
+ this.remoteVideo.play();
+ }
+ }.bind(this);
+ }
+ /**
+ * Create the RTCPeerConnection for the person being called
+ * @param remoteDescription {RemoteDescription}
+ */
+ createTargetSession( remoteDescription ){
+
+
+
+ this.peerConnection.setRemoteDescription( remoteDescription ).then( () => {
+
+ this.peerConnection.createAnswer().then( answer => {
+ this.peerConnection.setLocalDescription( answer ).catch( err => {
+ this.onError(err, answer);
+ });
+ this.stream.emit( 'video_message', JSON.stringify({ answer }) );
+ }).catch( err => {
+ this.onError(err, remoteDescription);
+ });
+ }).catch( err => {
+ this.onError(err, remoteDescription);
+ });
+
+ }
+ createCallSession( ){
+ this.peerConnection.createOffer().then( offer => {
+ this.peerConnection.setLocalDescription( offer ).catch( err => {
+ this.onError(err, offer);
+ });
+ this.stream.emit( 'video_message', JSON.stringify({ offer }) );
+ }).catch( err => this.onError(err));
+ }
+ /**
+ * Call allows you to call a remote user using their userId
+ * @param _id {string}
+ */
+ call(_id, local, remote) {
+ if (local)
+ this.localVideo = local;
+ if (remote)
+ this.remoteVideo = remote;
+ Meteor.call('VideoCallServices/call', _id, ( err, _id )=>{
+
+ if(err)
+ this.onError(err, _id);
+ else {
+ this.stream = new Meteor.Streamer(_id);
+ this.stream.on('video_message', (stream_data) => {
+ if(typeof stream_data == 'string')
+ stream_data = JSON.parse(stream_data);
+ if( stream_data.answer ){
+ this.peerConnection.setRemoteDescription( stream_data.answer ).catch( err => {
+ this.onError(err, stream_data)
+ });
+ }
+ if( stream_data.candidate ){
+ if( typeof stream_data.candidate == 'string' )
+ stream_data.candidate = JSON.parse(stream_data.candidate);
+
+ this.peerConnection.addIceCandidate( stream_data.candidate ).catch( err => {
+ this.onError(err, stream_data);
+ });
+ }
+ });
+ }
+ });
+ }
+ /**
+ * Answer the phone call
+ * @param local {HTMLElement}
+ * @param remote {HTMLElement}
+ */
+ answerPhoneCall(local, remote){
+ if (local)
+ this.localVideo = local;
+ if (remote)
+ this.remoteVideo = remote;
+ Meteor.call('VideoCallServices/answer', err => {
+ if(err)
+ this.onError(err);
+ });
+ }
+ /**
+ * End the phone call
+ */
+ endPhoneCall(){
+ Meteor.call("VideoCallServices/end", err => {
+ if(err)
+ this.onError(err);
+ });
+ }
+
+ onTargetAccept(){
+
+ }
+ onReceivePhoneCall(fields){
+
+ }
+ onTerminateCall(){
+
+ }
+ onPeerConnectionCreated(){
+
+ }
+ onError(err){
+
+ }
+}
+
+
+Meteor.VideoCallServices = new VideoCallServices();
\ No newline at end of file
diff --git a/packages/elmarti_video-chat/services/publish.js b/packages/elmarti_video-chat/services/publish.js
new file mode 100644
index 0000000..dce7b00
--- /dev/null
+++ b/packages/elmarti_video-chat/services/publish.js
@@ -0,0 +1,17 @@
+import { Meteor } from 'meteor/meteor';
+import CallLog from './call_log';
+Meteor.publish('VideoChatPublication', function() {
+ return CallLog.find({
+ $or: [{
+ caller: this.userId,
+ status:{
+ $ne:"FINISHED"
+ }
+ }, {
+ target: this.userId,
+ status:{
+ $ne:"FINISHED"
+ }
+ }]
+ });
+});
\ No newline at end of file
diff --git a/packages/elmarti_video-chat/services/server.js b/packages/elmarti_video-chat/services/server.js
new file mode 100644
index 0000000..bc25aca
--- /dev/null
+++ b/packages/elmarti_video-chat/services/server.js
@@ -0,0 +1,175 @@
+import {Meteor} from 'meteor/meteor';
+import {check} from 'meteor/check';
+import CallLog from './call_log';
+Meteor.users.find({"status.online": true}).observe({
+ removed: function ({_id}) {
+ CallLog.find({
+ $or: [{
+ status: {
+ $ne: 'FINISHED'
+ },
+ target: _id
+ }, {
+ status: {
+ $ne: 'FINISHED'
+ },
+ caller: _id
+ }]
+ }).forEach(call =>
+ CallLog.update({
+ _id: call._id
+ }, {
+ $set: {
+ status: 'FINISHED'
+ }
+ }));
+ }
+});
+const streams = {};
+const services = {
+ /**
+ * Call allows you to call a remote user using their userId
+ * @param _id {string}
+ */
+ call(_id){
+ check(_id, String);
+ const meteorUser = Meteor.user();
+ if (!meteorUser) {
+ const err = new Meteor.Error(403, "USER_NOT_LOGGED_IN", {
+ caller: meteorUser._id,
+ target: _id
+ });
+ Meteor.VideoCallServices.onError(err);
+ throw err;
+ }
+ if (services.checkConnect(meteorUser._id, _id)) {
+ const inCall = CallLog.findOne({
+ status: "CONNECTED",
+ target: _id
+ });
+ if (inCall) {
+ const err = new Meteor.Error(500, "TARGET_IN_CALL", inCall);
+ Meteor.VideoCallServices.onError(err, inCall, Meteor.userId());
+ throw err;
+ }
+ else {
+ CallLog.update({
+ $or: [{
+ status: {
+ $ne: "FINISHED"
+ },
+ caller: meteorUser._id
+ }, {
+ status: {
+ $ne: "FINISHED"
+ },
+ target: meteorUser._id
+ }]
+
+ }, {
+ $set: {
+ status: "FINISHED"
+ }
+ });
+ const logId = CallLog.insert({
+ status: "NEW",
+ target: _id,
+ caller: meteorUser._id
+ });
+ streams[logId] = new Meteor.Streamer(logId);
+ streams[logId].allowRead('all');
+ streams[logId].allowWrite('all');
+ return logId;
+ }
+ } else {
+ const err = new Meteor.Error(403, "CONNECTION_NOT_ALLOWED", {
+ target: meteorUser._id,
+ caller: _id
+ });
+ Meteor.VideoCallServices.onError(err, _id, meteorUser);
+ throw err;
+ }
+
+ },
+ /**
+ * Check if call connection should be permitted
+ * @param _id {caller}
+ * @param _id {target}
+ * @returns boolean
+ */
+ checkConnect(caller, target){
+ return true;
+ },
+ /**
+ * Answer current phone call
+ */
+ answer(){
+ const user = Meteor.user();
+ if (!user) {
+ const err = new Meteor.Error(403, "USER_NOT_LOGGED_IN");
+ Meteor.VideoCallServices.onError(err);
+ throw err;
+ }
+ const session = CallLog.findOne({
+ target: user._id,
+ status: 'NEW'
+ });
+ if (!session) {
+ const err = new Meteor.Error(500, 'SESSION_NOT_FOUND', {
+ target: user._id
+ });
+ Meteor.VideoCallServices.onError(err, undefined, user);
+ throw err;
+ }
+
+ else {
+ CallLog.update({
+ _id: session._id
+ }, {
+ $set: {
+ status: 'ACCEPTED'
+ }
+ });
+ }
+ },
+ /**
+ * End current phone call
+ */
+ end(){
+ const _id = Meteor.userId();
+ CallLog.find({
+ $or: [{
+ status: {
+ $ne: 'FINISHED'
+ },
+ target: _id
+ }, {
+ status: {
+ $ne: 'FINISHED'
+ },
+ caller: _id
+ }]
+ }).forEach(call =>
+ CallLog.update({
+ _id: call._id
+ }, {
+ $set: {
+ status: 'FINISHED'
+ }
+ }));
+ }
+};
+Meteor.methods({
+ 'VideoCallServices/call': services.call,
+ 'VideoCallServices/answer': services.answer,
+ 'VideoCallServices/end': services.end
+});
+Meteor.VideoCallServices = {
+ /**
+ * Callback envoked on error
+ * @param err {Error}
+ * @param data {Object}
+ * @param user {Object}
+ */
+ onError(err, data, user){}
+};
\ No newline at end of file
diff --git a/packages/elmarti_video-chat/video-chat-tests.js b/packages/elmarti_video-chat/video-chat-tests.js
new file mode 100644
index 0000000..e69de29
diff --git a/packages/meteoric_ionic/components/ionPopover/ionPopover.js b/packages/meteoric_ionic/components/ionPopover/ionPopover.js
index a328e3e..acde8c2 100644
--- a/packages/meteoric_ionic/components/ionPopover/ionPopover.js
+++ b/packages/meteoric_ionic/components/ionPopover/ionPopover.js
@@ -32,13 +32,15 @@ IonPopover = {
if (buttonPosition.top + buttonHeight + popoverHeight > bodyHeight) {
popoverCSS.top = buttonPosition.top - popoverHeight;
- $popover.addClass('popover-bottom');
+ $popover.addClass('popover-bottom animated slideInUp');
} else {
popoverCSS.top = buttonPosition.top + buttonHeight;
$popover.removeClass('popover-bottom');
+ $popover.addClass('popover-bottom animated slideInDown');
}
$backdrop.addClass('active');
+
$arrow.css({
left: buttonPosition.left + buttonWidth / 2 - $arrow.outerWidth() / 2 - popoverCSS.left + 'px'
});
@@ -48,11 +50,13 @@ IonPopover = {
hide: function () {
if (typeof this.view !== 'undefined') {
var $backdrop = $(this.view.firstNode());
- $backdrop.removeClass('active');
-
+
+
var $popover = $backdrop.find('.popover');
- $popover.css({opacity: 0});
-
+ $popover.addClass('fadeOut');
+ $popover.removeClass('slideInDown');
+ //$popover.css({opacity: 0});
+ //$backdrop.removeClass('active');
Blaze.remove(this.view);
}
}
diff --git a/packages/meteoric_ionic/components/ionPopup/ionPopup.html b/packages/meteoric_ionic/components/ionPopup/ionPopup.html
index f6aeadf..2ca785d 100644
--- a/packages/meteoric_ionic/components/ionPopup/ionPopup.html
+++ b/packages/meteoric_ionic/components/ionPopup/ionPopup.html
@@ -5,7 +5,7 @@
{{#if hasHead}}
{{#if title}}
-
{{title}}
+
{{{title}}}
{{/if}}
{{#if subTitle}}
{{subTitle}}
diff --git a/packages/mizzao_timesync/.gitignore b/packages/mizzao_timesync/.gitignore
new file mode 100644
index 0000000..3ccf4f8
--- /dev/null
+++ b/packages/mizzao_timesync/.gitignore
@@ -0,0 +1,2 @@
+.build*
+.versions
diff --git a/packages/mizzao_timesync/.travis.yml b/packages/mizzao_timesync/.travis.yml
new file mode 100644
index 0000000..d826297
--- /dev/null
+++ b/packages/mizzao_timesync/.travis.yml
@@ -0,0 +1,5 @@
+language: node_js
+node_js:
+ - "0.10"
+before_install:
+ - "curl -L http://git.io/ejPSng | /bin/sh"
diff --git a/packages/mizzao_timesync/History.md b/packages/mizzao_timesync/History.md
new file mode 100644
index 0000000..4752e66
--- /dev/null
+++ b/packages/mizzao_timesync/History.md
@@ -0,0 +1,81 @@
+## vNEXT
+
+## v0.5.0
+
+- guess new offset instead of unsetting if the client time has changed. This prevents that `TimeSync.serverTime` returns `undefined` after the time has changed and the client isn't in sync with the server.
+
+## v0.4.0
+
+- Update CORS headers to support Meteor 1.3. (#37, #41)
+- Support Meteor apps running in sub-paths instead of at root level. (#36, #40)
+
+## v0.3.4
+
+- Explicitly pull in client-side `check` for Meteor 1.2 apps.
+
+## v0.3.3
+
+- Be more robust with sync url when outside of Cordova. (#30)
+
+## v0.3.2
+
+- Fix issue when used in Cordova. (#22, #26, #27)
+
+## v0.3.1
+
+- Fix an issue where `TimeSync.serverTime` returned an erroneous value when passed a `Date` (instead of an epoch). (#23)
+
+## v0.3.0
+
+- `TimeSync.serverTime` now supports an optional second `updateInterval` argument, causing the reactive value to update less frequently. (#10)
+- `TimeSync.loggingEnabled` can be now set to false to suppress client log output. (#21)
+- Explicitly set MIME type on timesync endpoint. (#17, #18)
+
+## v0.2.2
+
+- **Updated for Meteor 0.9.**
+- Further adjust clock watching tolerance to be less sensitive to CPU.
+
+## v0.2.1
+
+- Re-sync automatically after a reconnection.
+- Adjust clock watching tolerance so as to be less sensitive to heavy client CPU usage.
+
+## v0.2.0
+
+- Clock change watching is now on by default (it's very lightweight and only involves grabbing and checking a `Date`).
+- Invalidate offset value and dependent time computations when we detect a clock change.
+- Added a `Date.now` shim for earlier versions of IE.
+- Reorganized code for testing and added some basic tests.
+
+## v0.1.6
+
+- Added the optional `TimeSync.watchClockChanges` which can resync if a client's clock is detected to have significantly changed.
+- Added retry attempts to syncing, making it more robust over a hot code reload among other situations.
+
+## v0.1.5
+
+- Use `WebApp.rawConnectHandlers` as a less janky way of getting our date request handled first.
+- Fixed an issue where a cached reload could result in a wacky time offset due to the server time being cached.
+
+## v0.1.4
+
+- Switch to JS at the request of @raix and @arunoda ;-)
+- Use a middleware handler, spliced into the top of the connect stack, instead of a Meteor method to avoid arbitrary method blocking delay. This improves accuracy significantly.
+- Compute a RTT value in `TimeSync.roundTripTime` as well as a time offset.
+
+## v0.1.3
+
+- Ensure that the computed offset is always an integer number of milliseconds.
+
+## v0.1.2
+
+- Added the `TimeSync.resync` function that triggers a resync with the server.
+
+## v0.1.1
+
+- Added the reactive function `TimeSync.isSynced` to determine if an initial sync has taken place.
+
+## v0.1.0
+
+- First release.
diff --git a/packages/mizzao_timesync/LICENSE b/packages/mizzao_timesync/LICENSE
new file mode 100644
index 0000000..d740546
--- /dev/null
+++ b/packages/mizzao_timesync/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Andrew Mao
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/packages/mizzao_timesync/README.md b/packages/mizzao_timesync/README.md
new file mode 100644
index 0000000..e194c6b
--- /dev/null
+++ b/packages/mizzao_timesync/README.md
@@ -0,0 +1,38 @@
+meteor-timesync [](https://travis-ci.org/mizzao/meteor-timesync)
+===============
+
+NTP-style time synchronization between server and client, and facilities to
+use server time reactively in Meteor applications.
+
+## What's this do?
+
+Meteor clients don't necessarily have accurate timestamps relative to your server. This package computes and maintains an offset between server and client, allowing server timestamps to be used on the client (especially for displaying time differences). It also provides facilities to use time reactively in your application.
+
+There is a demo as part of the user-status app at http://user-status.meteor.com.
+
+## Installation
+
+```
+meteor add mizzao:timesync
+```
+
+## Usage
+
+- `TimeSync.serverTime(clientTime, updateInterval)`: returns the server time for a given client time, as a UTC/Unix timestamp. A reactive variable which changes with the computed offset, and updates continually. Pass in `clientTime` optionally to specify a particular time on the client, instead of reactively depending on the current time. Pass in `updateInterval` to change the rate (in milliseconds) at which the reactive variable updates; the default value is 1000 (1 second).
+- `TimeSync.serverOffset()`: returns the current time difference between the server and the client. Reactively updates as the offset is recomputed.
+- `TimeSync.roundTripTime()`: The round trip ping to the server. Also reactive.
+- `TimeSync.isSynced()`: Reactive variable that determines if an initial sync has taken place.
+- `TimeSync.resync()`: Re-triggers a sync with the server. Can be useful because the initial sync often takes place during a lot of traffic with the server and could be less accurate.
+- `TimeSync.loggingEnabled`: defaults to `true`, set this to `false` to suppress diagnostic syncing messages on the client.
+
+To use the above functions in a non-reactive context, use [`Deps.nonreactive`](http://docs.meteor.com/#deps_nonreactive). This is useful if you are displaying a lot of timestamps or differences on a page and you don't want them to be constantly recomputed on the client. However, displaying time reactively should be pretty efficient with Meteor 0.8.0+ (Blaze).
+
+Note that `TimeSync.serverTime` returns a timestamp, not a `Date`, but you can easily construct a date with `new Date(TimeSync.serverTime(...))`.
+
+You can also use something like `TimeSync.serverTime(null, 5000)` to get a reactive time value that only updates at 5 second intervals. All reactive time variables with the same value of `updateInterval` are guaranteed to be invalidated at the same time.
+
+## Notes
+
+- This library is a crude approximation of NTP, at the moment. It's empirically shown to be accurate to under 100 ms on the meteor.com servers.
+- We could definitely do something smarter and more accurate, with multiple measurements and exponentially weighted updating.
+- Check out the moment library [packaged for meteor](https://github.com/acreeger/meteor-moment) for formatting and displaying the differences computed by this package.
diff --git a/packages/mizzao_timesync/package.js b/packages/mizzao_timesync/package.js
new file mode 100644
index 0000000..48f8f51
--- /dev/null
+++ b/packages/mizzao_timesync/package.js
@@ -0,0 +1,40 @@
+Package.describe({
+ name: "mizzao:timesync",
+ summary: "NTP-style time synchronization between server and client",
+ version: "0.5.0",
+ git: "https://github.com/mizzao/meteor-timesync.git"
+});
+
+Package.onUse(function (api) {
+ api.versionsFrom("1.2.0.1");
+
+ api.use([
+ 'check',
+ 'tracker',
+ 'http'
+ ], 'client');
+
+ api.use('webapp', 'server');
+
+ api.use('ecmascript');
+
+ // Our files
+ api.addFiles('timesync-server.js', 'server');
+ api.addFiles('timesync-client.js', 'client');
+
+ api.export('TimeSync', 'client');
+ api.export('SyncInternals', 'client', {testOnly: true} );
+});
+
+Package.onTest(function (api) {
+ api.use([
+ 'tinytest',
+ 'test-helpers'
+ ]);
+
+ api.use(["tracker", "underscore"], 'client');
+
+ api.use("mizzao:timesync");
+
+ api.addFiles('tests/client.js', 'client');
+});
diff --git a/packages/mizzao_timesync/tests/client.js b/packages/mizzao_timesync/tests/client.js
new file mode 100644
index 0000000..3a015b1
--- /dev/null
+++ b/packages/mizzao_timesync/tests/client.js
@@ -0,0 +1,118 @@
+Tinytest.add("timesync - tick check - normal tick", function(test) {
+ var lastTime = 5000;
+ var currentTime = 6000;
+ var interval = 1000;
+
+ test.equal(SyncInternals.getDiscrepancy(lastTime, currentTime, interval), 0);
+});
+
+Tinytest.add("timesync - tick check - slightly off", function(test) {
+ var lastTime = 5000;
+ var currentTime = 6500;
+ var interval = 1000;
+
+ test.equal(SyncInternals.getDiscrepancy(lastTime, currentTime, interval), 500);
+
+ currentTime = 5500;
+
+ test.equal(SyncInternals.getDiscrepancy(lastTime, currentTime, interval), -500);
+});
+
+Tinytest.add("timesync - tick check - big jump", function(test) {
+ var lastTime = 5000;
+ var currentTime = 0;
+ var interval = 1000;
+
+ test.equal(SyncInternals.getDiscrepancy(lastTime, currentTime, interval), -6000);
+
+ currentTime = 10000;
+
+ test.equal(SyncInternals.getDiscrepancy(lastTime, currentTime, interval), 4000);
+});
+
+/*
+ TODO: add tests for proper dependencies in reactive functions
+ */
+
+Tinytest.addAsync("timesync - basic - initial sync", function(test, next) {
+
+ function success() {
+ var syncedTime = TimeSync.serverTime();
+
+ // Make sure the time exists
+ test.isTrue(syncedTime);
+
+ // Make sure it's close to the current time on the client. This should
+ // always be true in PhantomJS tests where client/server are the same
+ // machine, although it might fail in development environments, for example
+ // when the server and client are different VMs.
+ test.isTrue( Math.abs(syncedTime - Date.now()) < 1000 );
+
+ next();
+ }
+
+ function fail() {
+ test.fail();
+ next();
+ }
+
+ simplePoll(TimeSync.isSynced, success, fail, 5000, 100);
+});
+
+Tinytest.addAsync("timesync - basic - serverTime format", function(test, next) {
+
+ test.isTrue(_.isNumber( TimeSync.serverTime() ));
+
+ test.isTrue(_.isNumber( TimeSync.serverTime(null) ));
+
+ // Accept Date as client time
+ test.isTrue(_.isNumber( TimeSync.serverTime(new Date()) ));
+
+ // Accept epoch as client time
+ test.isTrue(_.isNumber( TimeSync.serverTime(Date.now()) ));
+
+ next();
+});
+
+Tinytest.addAsync("timesync - basic - different sync intervals", function(test, next) {
+
+ var aCount = 0, bCount = 0, cCount = 0;
+
+ var a = Tracker.autorun(function () {
+ TimeSync.serverTime(null, 500);
+ aCount++;
+ });
+
+ var b = Tracker.autorun(function () {
+ TimeSync.serverTime();
+ bCount++;
+ });
+
+ var c = Tracker.autorun(function () {
+ TimeSync.serverTime(null, 2000);
+ cCount++;
+ });
+
+ var testInterval = 4990;
+
+ Meteor.setTimeout(function() {
+
+ test.equal(aCount, 10); // 0, 500, 1000, 1500 ...
+ // not going to be 5 since the first tick won't generate this dep
+ test.equal(bCount, 6);
+ test.equal(cCount, 3); // 0, 2000, 4000
+
+ test.isTrue(SyncInternals.timeTick[500]);
+ test.isTrue(SyncInternals.timeTick[1000]);
+ test.isTrue(SyncInternals.timeTick[2000]);
+
+ test.equal(Object.keys(SyncInternals.timeTick).length, 3);
+
+ a.stop();
+ b.stop();
+ c.stop();
+
+ next()
+ }, testInterval);
+
+});
diff --git a/packages/mizzao_timesync/timesync-client.js b/packages/mizzao_timesync/timesync-client.js
new file mode 100644
index 0000000..0fb939b
--- /dev/null
+++ b/packages/mizzao_timesync/timesync-client.js
@@ -0,0 +1,171 @@
+//IE8 doesn't have Date.now()
+Date.now = Date.now || function() { return +new Date; };
+
+TimeSync = {
+ loggingEnabled: true
+};
+
+function log(/* arguments */) {
+ if (TimeSync.loggingEnabled) {
+ Meteor._debug.apply(this, arguments);
+ }
+}
+
+var defaultInterval = 1000;
+
+// Internal values, exported for testing
+SyncInternals = {
+ offset: undefined,
+ roundTripTime: undefined,
+ offsetDep: new Deps.Dependency(),
+ syncDep: new Deps.Dependency(),
+ isSynced: false,
+ timeTick: {},
+ getDiscrepancy: function (lastTime, currentTime, interval) {
+ return currentTime - (lastTime + interval)
+ }
+};
+
+SyncInternals.timeTick[defaultInterval] = new Deps.Dependency();
+
+var maxAttempts = 5;
+var attempts = 0;
+
+/*
+ This is an approximation of
+ http://en.wikipedia.org/wiki/Network_Time_Protocol
+
+ If this turns out to be more accurate under the connect handlers,
+ we should try taking multiple measurements.
+ */
+
+var syncUrl;
+if (Meteor.isCordova) {
+ // Only use Meteor.absoluteUrl for Cordova; see
+ // https://github.com/meteor/meteor/issues/4696
+ // https://github.com/mizzao/meteor-timesync/issues/30
+ // Cordova should never be running out of a subdirectory...
+ syncUrl = Meteor.absoluteUrl("_timesync");
+}
+else {
+ // Support Meteor running in relative paths, based on computed root url prefix
+ // https://github.com/mizzao/meteor-timesync/pull/40
+ const basePath = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || '';
+ syncUrl = basePath + "/_timesync";
+}
+
+var updateOffset = function() {
+ var t0 = Date.now();
+
+ HTTP.get(syncUrl, function(err, response) {
+ var t3 = Date.now(); // Grab this now
+ if (err) {
+ // We'll still use our last computed offset if is defined
+ log("Error syncing to server time: ", err);
+ if (++attempts <= maxAttempts)
+ Meteor.setTimeout(TimeSync.resync, 1000);
+ else
+ log("Max number of time sync attempts reached. Giving up.");
+ return;
+ }
+
+ attempts = 0; // It worked
+
+ var ts = parseInt(response.content);
+ SyncInternals.isSynced = true;
+ SyncInternals.offset = Math.round(((ts - t0) + (ts - t3)) / 2);
+ SyncInternals.roundTripTime = t3 - t0; // - (ts - ts) which is 0
+ SyncInternals.offsetDep.changed();
+ });
+};
+
+// Reactive variable for server time that updates every second.
+TimeSync.serverTime = function(clientTime, interval) {
+ check(interval, Match.Optional(Match.Integer));
+ // If a client time is provided, we don't need to depend on the tick.
+ if ( !clientTime ) getTickDependency(interval || defaultInterval).depend();
+
+ SyncInternals.offsetDep.depend(); // depend on offset to enable reactivity
+ // Convert Date argument to epoch as necessary
+ return (+clientTime || Date.now()) + SyncInternals.offset;
+};
+
+// Reactive variable for the difference between server and client time.
+TimeSync.serverOffset = function() {
+ SyncInternals.offsetDep.depend();
+ return SyncInternals.offset;
+};
+
+TimeSync.roundTripTime = function() {
+ SyncInternals.offsetDep.depend();
+ return SyncInternals.roundTripTime;
+};
+
+TimeSync.isSynced = function() {
+ SyncInternals.offsetDep.depend();
+ return SyncInternals.isSynced;
+};
+
+var resyncIntervalId = null;
+
+TimeSync.resync = function() {
+ if (resyncIntervalId !== null) Meteor.clearInterval(resyncIntervalId);
+ updateOffset();
+ resyncIntervalId = Meteor.setInterval(updateOffset, 600000);
+};
+
+// Run this as soon as we load, even before Meteor.startup()
+// Run again whenever we reconnect after losing connection
+var wasConnected = false;
+
+Deps.autorun(function() {
+ var connected = Meteor.status().connected;
+ if ( connected && !wasConnected ) TimeSync.resync();
+ wasConnected = connected;
+});
+
+// Resync if unexpected change by more than a few seconds. This needs to be
+// somewhat lenient, or a CPU-intensive operation can trigger a re-sync even
+// when the offset is still accurate. In any case, we're not going to be able to
+// catch very small system-initiated NTP adjustments with this, anyway.
+var tickCheckTolerance = 5000;
+
+var lastClientTime = Date.now();
+
+// Set up a new interval for any amount of reactivity.
+function getTickDependency(interval) {
+
+ if ( !SyncInternals.timeTick[interval] ) {
+ var dep = new Deps.Dependency();
+
+ Meteor.setInterval(function() {
+ dep.changed();
+ }, interval);
+
+ SyncInternals.timeTick[interval] = dep;
+ }
+
+ return SyncInternals.timeTick[interval];
+}
+
+// Set up special interval for the default tick, which also watches for re-sync
+Meteor.setInterval(function() {
+ var currentClientTime = Date.now();
+
+ var discrepancy = SyncInternals.getDiscrepancy(lastClientTime, currentClientTime, defaultInterval);
+ if (Math.abs(discrepancy) < tickCheckTolerance) {
+ // No problem here, just keep ticking along
+ SyncInternals.timeTick[defaultInterval].changed();
+ } else {
+ // resync on major client clock changes
+ // based on http://stackoverflow.com/a/3367542/1656818
+ log("Clock discrepancy detected. Attempting re-sync.");
+ // Refuse to compute server time and try to guess new server offset. Guessing only works if the server time hasn't changed.
+ SyncInternals.offset = SyncInternals.offset - discrepancy;
+ SyncInternals.isSynced = false;
+ SyncInternals.offsetDep.changed();
+ TimeSync.resync();
+ }
+
+ lastClientTime = currentClientTime;
+}, defaultInterval);
diff --git a/packages/mizzao_timesync/timesync-server.js b/packages/mizzao_timesync/timesync-server.js
new file mode 100644
index 0000000..6f6bf1a
--- /dev/null
+++ b/packages/mizzao_timesync/timesync-server.js
@@ -0,0 +1,28 @@
+// Use rawConnectHandlers so we get a response as quickly as possible
+// https://github.com/meteor/meteor/blob/devel/packages/webapp/webapp_server.js
+
+WebApp.rawConnectHandlers.use("/_timesync",
+ function(req, res, next) {
+ // Never ever cache this, otherwise weird times are shown on reload
+ // http://stackoverflow.com/q/18811286/586086
+ res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ res.setHeader("Pragma", "no-cache");
+ res.setHeader("Expires", 0);
+
+ // Avoid MIME type warnings in browsers
+ res.setHeader("Content-Type", "text/plain");
+
+ // Cordova lives in a local webserver, so it does CORS
+ // we need to bless it's requests in order for it to accept our results
+ // Match http://localhost: for Cordova clients in Meteor 1.3
+ // and http://meteor.local for earlier versions
+ const origin = req.headers.origin;
+
+ if (origin && ( origin === 'http://meteor.local' ||
+ /^http:\/\/localhost:1[23]\d\d\d$/.test(origin) ) ) {
+ res.setHeader('Access-Control-Allow-Origin', origin);
+ }
+
+ res.end(Date.now().toString());
+ }
+);
diff --git a/packages/plugin.xml b/packages/plugin.xml
new file mode 100644
index 0000000..c471881
--- /dev/null
+++ b/packages/plugin.xml
@@ -0,0 +1,23 @@
+
+
+ Cordova Crosswalk Permissions
+ Apache 2.0 License
+ Dispatch
+
+ Request the necessary crosswalk permissions on Android for a Cordova project.
+
+
+
+ This plugin replaces the need for adding permisions to the AndroidManifest.xml
+ file when using Crosswalk with Cordova.
+
+
+
+
+
+
+
+