Initial work on Edge

master
Iñaki Baz Castillo 2017-06-06 19:22:13 +02:00
parent 2ea269ea9a
commit 0c63f4cd8c
11 changed files with 3148 additions and 62 deletions

View File

@ -5,8 +5,8 @@ import browser from 'bowser';
import sdpTransform from 'sdp-transform'; import sdpTransform from 'sdp-transform';
import Logger from './Logger'; import Logger from './Logger';
import protooClient from 'protoo-client'; import protooClient from 'protoo-client';
import urlFactory from './urlFactory'; import * as urlFactory from './urlFactory';
import utils from './utils'; import * as utils from './utils';
const logger = new Logger('Client'); const logger = new Logger('Client');

View File

@ -2,12 +2,13 @@
import React from 'react'; import React from 'react';
import ClipboardButton from 'react-clipboard.js'; import ClipboardButton from 'react-clipboard.js';
import browser from 'bowser';
import TransitionAppear from './TransitionAppear'; import TransitionAppear from './TransitionAppear';
import LocalVideo from './LocalVideo'; import LocalVideo from './LocalVideo';
import RemoteVideo from './RemoteVideo'; import RemoteVideo from './RemoteVideo';
import Stats from './Stats'; import Stats from './Stats';
import Logger from '../Logger'; import Logger from '../Logger';
import utils from '../utils'; import * as utils from '../utils';
import Client from '../Client'; import Client from '../Client';
const logger = new Logger('Room'); const logger = new Logger('Room');
@ -285,8 +286,8 @@ export default class Room extends React.Component
imageHeight : 80 imageHeight : 80
}); });
// Start retrieving WebRTC stats (unless mobile) // Start retrieving WebRTC stats (unless mobile or Edge).
if (utils.isDesktop()) if (utils.isDesktop() && !browser.msedge)
{ {
this.setState({ showStats: true }); this.setState({ showStats: true });
@ -475,10 +476,10 @@ export default class Room extends React.Component
this.setState({ stats: null }); this.setState({ stats: null });
this._statsTimer = setTimeout(() => // this._statsTimer = setTimeout(() =>
{ // {
getStats.call(this); // getStats.call(this);
}, STATS_INTERVAL); // }, STATS_INTERVAL);
}); });
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,105 @@
import sdpTransform from 'sdp-transform';
/**
* RTCSessionDescription implementation.
*/
export default class RTCSessionDescription {
/**
* RTCSessionDescription constructor.
* @param {Object} [data]
* @param {String} [data.type] - 'offer' / 'answer'.
* @param {String} [data.sdp] - SDP string.
* @param {Object} [data._sdpObject] - SDP object generated by the
* sdp-transform library.
*/
constructor(data) {
// @type {String}
this._sdp = null;
// @type {Object}
this._sdpObject = null;
// @type {String}
this._type = null;
switch (data.type) {
case 'offer':
break;
case 'answer':
break;
default:
throw new TypeError(`invalid type "${data.type}"`);
}
this._type = data.type;
if (typeof data.sdp === 'string') {
this._sdp = data.sdp;
try {
this._sdpObject = sdpTransform.parse(data.sdp);
} catch (error) {
throw new Error(`invalid sdp: ${error}`);
}
} else if (typeof data._sdpObject === 'object') {
this._sdpObject = data._sdpObject;
try {
this._sdp = sdpTransform.write(data._sdpObject);
} catch (error) {
throw new Error(`invalid sdp object: ${error}`);
}
} else {
throw new TypeError('invalid sdp or _sdpObject');
}
}
/**
* Get sdp field.
* @return {String}
*/
get sdp() {
return this._sdp;
}
/**
* Set sdp field.
* NOTE: This is not allowed per spec, but lib-jitsi-meet uses it.
* @param {String} sdp
*/
set sdp(sdp) {
try {
this._sdpObject = sdpTransform.parse(sdp);
} catch (error) {
throw new Error(`invalid sdp: ${error}`);
}
this._sdp = sdp;
}
/**
* Gets the internal sdp object.
* @return {Object}
* @private
*/
get sdpObject() {
return this._sdpObject;
}
/**
* Get type field.
* @return {String}
*/
get type() {
return this._type;
}
/**
* Returns an object with type and sdp fields.
* @return {Object}
*/
toJSON() {
return {
sdp: this._sdp,
type: this._type
};
}
}

View File

@ -0,0 +1,21 @@
/**
* Create a class inheriting from Error.
*/
function createErrorClass(name) {
const klass = class extends Error {
/**
* Custom error class constructor.
* @param {string} message
*/
constructor(message) {
super(message);
// Override `name` property value and make it non enumerable.
Object.defineProperty(this, 'name', { value: name });
}
};
return klass;
}
export const InvalidStateError = createErrorClass('InvalidStateError');

View File

@ -0,0 +1,458 @@
/* global RTCRtpReceiver */
import sdpTransform from 'sdp-transform';
/**
* Extract RTP capabilities from remote description.
* @param {Object} sdpObject - Remote SDP object generated by sdp-transform.
* @return {RTCRtpCapabilities}
*/
export function extractCapabilities(sdpObject) {
// Map of RtpCodecParameters indexed by payload type.
const codecsMap = new Map();
// Array of RtpHeaderExtensions.
const headerExtensions = [];
for (const m of sdpObject.media) {
// Media kind.
const kind = m.type;
if (kind !== 'audio' && kind !== 'video') {
continue; // eslint-disable-line no-continue
}
// Get codecs.
for (const rtp of m.rtp) {
const codec = {
clockRate: rtp.rate,
kind,
mimeType: `${kind}/${rtp.codec}`,
name: rtp.codec,
numChannels: rtp.encoding || 1,
parameters: {},
preferredPayloadType: rtp.payload,
rtcpFeedback: []
};
codecsMap.set(codec.preferredPayloadType, codec);
}
// Get codec parameters.
for (const fmtp of m.fmtp || []) {
const parameters = sdpTransform.parseFmtpConfig(fmtp.config);
const codec = codecsMap.get(fmtp.payload);
if (!codec) {
continue; // eslint-disable-line no-continue
}
codec.parameters = parameters;
}
// Get RTCP feedback for each codec.
for (const fb of m.rtcpFb || []) {
const codec = codecsMap.get(fb.payload);
if (!codec) {
continue; // eslint-disable-line no-continue
}
codec.rtcpFeedback.push({
parameter: fb.subtype || '',
type: fb.type
});
}
// Get RTP header extensions.
for (const ext of m.ext || []) {
const preferredId = ext.value;
const uri = ext.uri;
const headerExtension = {
kind,
uri,
preferredId
};
// Check if already present.
const duplicated = headerExtensions.find(savedHeaderExtension =>
headerExtension.kind === savedHeaderExtension.kind
&& headerExtension.uri === savedHeaderExtension.uri
);
if (!duplicated) {
headerExtensions.push(headerExtension);
}
}
}
return {
codecs: Array.from(codecsMap.values()),
fecMechanisms: [], // TODO
headerExtensions
};
}
/**
* Extract DTLS parameters from remote description.
* @param {Object} sdpObject - Remote SDP object generated by sdp-transform.
* @return {RTCDtlsParameters}
*/
export function extractDtlsParameters(sdpObject) {
const media = getFirstActiveMediaSection(sdpObject);
const fingerprint = media.fingerprint || sdpObject.fingerprint;
let role;
switch (media.setup) {
case 'active':
role = 'client';
break;
case 'passive':
role = 'server';
break;
case 'actpass':
role = 'auto';
break;
}
return {
role,
fingerprints: [
{
algorithm: fingerprint.type,
value: fingerprint.hash
}
]
};
}
/**
* Extract ICE candidates from remote description.
* NOTE: This implementation assumes a single BUNDLEd transport and rtcp-mux.
* @param {Object} sdpObject - Remote SDP object generated by sdp-transform.
* @return {sequence<RTCIceCandidate>}
*/
export function extractIceCandidates(sdpObject) {
const media = getFirstActiveMediaSection(sdpObject);
const candidates = [];
for (const c of media.candidates) {
// Ignore RTCP candidates (we assume rtcp-mux).
if (c.component !== 1) {
continue; // eslint-disable-line no-continue
}
const candidate = {
foundation: c.foundation,
ip: c.ip,
port: c.port,
priority: c.priority,
protocol: c.transport.toLowerCase(),
type: c.type
};
candidates.push(candidate);
}
return candidates;
}
/**
* Extract ICE parameters from remote description.
* NOTE: This implementation assumes a single BUNDLEd transport.
* @param {Object} sdpObject - Remote SDP object generated by sdp-transform.
* @return {RTCIceParameters}
*/
export function extractIceParameters(sdpObject) {
const media = getFirstActiveMediaSection(sdpObject);
const usernameFragment = media.iceUfrag;
const password = media.icePwd;
const icelite = sdpObject.icelite === 'ice-lite';
return {
icelite,
password,
usernameFragment
};
}
/**
* Extract MID values from remote description.
* @param {Object} sdpObject - Remote SDP object generated by sdp-transform.
* @return {map<String, String>} Ordered Map with MID as key and kind as value.
*/
export function extractMids(sdpObject) {
const midToKind = new Map();
// Ignore disabled media sections.
for (const m of sdpObject.media) {
midToKind.set(m.mid, m.type);
}
return midToKind;
}
/**
* Extract tracks information.
* @param {Object} sdpObject - Remote SDP object generated by sdp-transform.
* @return {Map}
*/
export function extractTrackInfos(sdpObject) {
// Map with info about receiving media.
// - index: Media SSRC
// - value: Object
// - kind: 'audio' / 'video'
// - ssrc: Media SSRC
// - rtxSsrc: RTX SSRC (may be unset)
// - streamId: MediaStream.jitsiRemoteId
// - trackId: MediaStreamTrack.jitsiRemoteId
// - cname: CNAME
// @type {map<Number, Object>}
const infos = new Map();
// Map with stream SSRC as index and associated RTX SSRC as value.
// @type {map<Number, Number>}
const rtxMap = new Map();
// Set of RTX SSRC values.
const rtxSet = new Set();
for (const m of sdpObject.media) {
const kind = m.type;
if (kind !== 'audio' && kind !== 'video') {
continue; // eslint-disable-line no-continue
}
// Get RTX information.
for (const ssrcGroup of m.ssrcGroups || []) {
// Just consider FID.
if (ssrcGroup.semantics !== 'FID') {
continue; // eslint-disable-line no-continue
}
const ssrcs
= ssrcGroup.ssrcs.split(' ').map(ssrc => Number(ssrc));
const ssrc = ssrcs[0];
const rtxSsrc = ssrcs[1];
rtxMap.set(ssrc, rtxSsrc);
rtxSet.add(rtxSsrc);
}
for (const ssrcObject of m.ssrcs || []) {
const ssrc = ssrcObject.id;
// Ignore RTX.
if (rtxSet.has(ssrc)) {
continue; // eslint-disable-line no-continue
}
let info = infos.get(ssrc);
if (!info) {
info = {
kind,
rtxSsrc: rtxMap.get(ssrc),
ssrc
};
infos.set(ssrc, info);
}
switch (ssrcObject.attribute) {
case 'cname': {
info.cname = ssrcObject.value;
break;
}
case 'msid': {
const values = ssrcObject.value.split(' ');
const streamId = values[0];
const trackId = values[1];
info.streamId = streamId;
info.trackId = trackId;
break;
}
case 'mslabel': {
const streamId = ssrcObject.value;
info.streamId = streamId;
break;
}
case 'label': {
const trackId = ssrcObject.value;
info.trackId = trackId;
break;
}
}
}
}
return infos;
}
/**
* Get local ORTC RTP capabilities filtered and adapted to the given remote RTP
* capabilities.
* @param {RTCRtpCapabilities} filterWithCapabilities - RTP capabilities to
* filter with.
* @return {RTCRtpCapabilities}
*/
export function getLocalCapabilities(filterWithCapabilities) {
const localFullCapabilities = RTCRtpReceiver.getCapabilities();
const localCapabilities = {
codecs: [],
fecMechanisms: [],
headerExtensions: []
};
// Map of RTX and codec payloads.
// - index: Codec payloadType
// - value: Associated RTX payloadType
// @type {map<Number, Number>}
const remoteRtxMap = new Map();
// Set codecs.
for (const remoteCodec of filterWithCapabilities.codecs) {
const remoteCodecName = remoteCodec.name.toLowerCase();
if (remoteCodecName === 'rtx') {
remoteRtxMap.set(
remoteCodec.parameters.apt, remoteCodec.preferredPayloadType);
continue; // eslint-disable-line no-continue
}
const localCodec = localFullCapabilities.codecs.find(codec =>
codec.name.toLowerCase() === remoteCodecName
&& codec.kind === remoteCodec.kind
&& codec.clockRate === remoteCodec.clockRate
);
if (!localCodec) {
continue; // eslint-disable-line no-continue
}
const codec = {
clockRate: localCodec.clockRate,
kind: localCodec.kind,
mimeType: `${localCodec.kind}/${localCodec.name}`,
name: localCodec.name,
numChannels: localCodec.numChannels || 1,
parameters: {},
preferredPayloadType: remoteCodec.preferredPayloadType,
rtcpFeedback: []
};
for (const remoteParamName of Object.keys(remoteCodec.parameters)) {
const remoteParamValue
= remoteCodec.parameters[remoteParamName];
for (const localParamName of Object.keys(localCodec.parameters)) {
const localParamValue
= localCodec.parameters[localParamName];
if (localParamName !== remoteParamName) {
continue; // eslint-disable-line no-continue
}
// TODO: We should consider much more cases here, but Edge
// does not support many codec parameters.
if (localParamValue === remoteParamValue) {
// Use this RTP parameter.
codec.parameters[localParamName] = localParamValue;
break;
}
}
}
for (const remoteFb of remoteCodec.rtcpFeedback) {
const localFb = localCodec.rtcpFeedback.find(fb =>
fb.type === remoteFb.type
&& fb.parameter === remoteFb.parameter
);
if (localFb) {
// Use this RTCP feedback.
codec.rtcpFeedback.push(localFb);
}
}
// Use this codec.
localCapabilities.codecs.push(codec);
}
// Add RTX for video codecs.
for (const codec of localCapabilities.codecs) {
const payloadType = codec.preferredPayloadType;
if (!remoteRtxMap.has(payloadType)) {
continue; // eslint-disable-line no-continue
}
const rtxCodec = {
clockRate: codec.clockRate,
kind: codec.kind,
mimeType: `${codec.kind}/rtx`,
name: 'rtx',
parameters: {
apt: payloadType
},
preferredPayloadType: remoteRtxMap.get(payloadType),
rtcpFeedback: []
};
// Add RTX codec.
localCapabilities.codecs.push(rtxCodec);
}
// Add RTP header extensions.
for (const remoteExtension of filterWithCapabilities.headerExtensions) {
const localExtension
= localFullCapabilities.headerExtensions.find(extension =>
extension.kind === remoteExtension.kind
&& extension.uri === remoteExtension.uri
);
if (localExtension) {
const extension = {
kind: localExtension.kind,
preferredEncrypt: Boolean(remoteExtension.preferredEncrypt),
preferredId: remoteExtension.preferredId,
uri: localExtension.uri
};
// Use this RTP header extension.
localCapabilities.headerExtensions.push(extension);
}
}
// Add FEC mechanisms.
// NOTE: We don't support FEC yet and, in fact, neither does Edge.
for (const remoteFecMechanism of filterWithCapabilities.fecMechanisms) {
const localFecMechanism
= localFullCapabilities.fecMechanisms.find(fec =>
fec === remoteFecMechanism
);
if (localFecMechanism) {
// Use this FEC mechanism.
localCapabilities.fecMechanisms.push(localFecMechanism);
}
}
return localCapabilities;
}
/**
* Get the first acive media section.
* @param {Object} sdpObject - SDP object generated by sdp-transform.
* @return {Object} SDP media section as parsed by sdp-transform.
*/
function getFirstActiveMediaSection(sdpObject) {
return sdpObject.media.find(m =>
m.iceUfrag && m.port !== 0
);
}

View File

@ -9,13 +9,26 @@ import ReactDOM from 'react-dom';
import injectTapEventPlugin from 'react-tap-event-plugin'; import injectTapEventPlugin from 'react-tap-event-plugin';
import randomString from 'random-string'; import randomString from 'random-string';
import Logger from './Logger'; import Logger from './Logger';
import utils from './utils'; import * as utils from './utils';
import edgeRTCPeerConnection from './edge/RTCPeerConnection';
import edgeRTCSessionDescription from './edge/RTCSessionDescription';
import App from './components/App'; import App from './components/App';
// TODO: TMP
global.BROWSER = browser;
const REGEXP_FRAGMENT_ROOM_ID = new RegExp('^#room-id=([0-9a-zA-Z_\-]+)$'); const REGEXP_FRAGMENT_ROOM_ID = new RegExp('^#room-id=([0-9a-zA-Z_\-]+)$');
const logger = new Logger(); const logger = new Logger();
logger.debug('detected browser [name:"%s", version:%s]', browser.name, browser.version); logger.warn('detected browser [name:"%s", version:%s]', browser.name, browser.version);
if (browser.msedge)
{
logger.warn('EDGE detected, overriding WebRTC global classes');
window.RTCPeerConnection = edgeRTCPeerConnection;
window.RTCSessionDescription = edgeRTCSessionDescription;
}
injectTapEventPlugin(); injectTapEventPlugin();

View File

@ -2,14 +2,11 @@
const config = require('../config'); const config = require('../config');
module.exports = export function getProtooUrl(peerId, roomId)
{ {
getProtooUrl(peerId, roomId)
{
let hostname = window.location.hostname; let hostname = window.location.hostname;
let port = config.protoo.listenPort; let port = config.protoo.listenPort;
let url = `wss://${hostname}:${port}/?peer-id=${peerId}&room-id=${roomId}`; let url = `wss://${hostname}:${port}/?peer-id=${peerId}&room-id=${roomId}`;
return url; return url;
} }
};

View File

@ -1,44 +1,54 @@
'use strict'; 'use strict';
import browser from 'bowser'; import browser from 'bowser';
import randomNumberLib from 'random-number';
import Logger from './Logger'; import Logger from './Logger';
const logger = new Logger('utils'); const logger = new Logger('utils');
const randomNumberGenerator = randomNumberLib.generator(
{
min : 10000000,
max : 99999999,
integer : true
});
let mediaQueryDetectorElem; let mediaQueryDetectorElem;
module.exports = export function initialize()
{ {
initialize()
{
logger.debug('initialize()'); logger.debug('initialize()');
// Media query detector stuff // Media query detector stuff
mediaQueryDetectorElem = document.getElementById('mediasoup-demo-app-media-query-detector'); mediaQueryDetectorElem = document.getElementById('mediasoup-demo-app-media-query-detector');
return Promise.resolve(); return Promise.resolve();
}, }
isDesktop() export function isDesktop()
{ {
return !!mediaQueryDetectorElem.offsetParent; return !!mediaQueryDetectorElem.offsetParent;
}, }
isMobile() export function isMobile()
{ {
return !mediaQueryDetectorElem.offsetParent; return !mediaQueryDetectorElem.offsetParent;
}, }
isPlanB() export function isPlanB()
{ {
if (browser.chrome || browser.chromium || browser.opera) if (browser.chrome || browser.chromium || browser.opera || browser.msedge)
return true; return true;
else else
return false; return false;
}, }
closeMediaStream(stream) export function randomNumber()
{ {
return randomNumberGenerator();
}
export function closeMediaStream(stream)
{
if (!stream) if (!stream)
return; return;
@ -48,5 +58,4 @@ module.exports =
{ {
tracks[i].stop(); tracks[i].stop();
} }
} }
};

View File

@ -15,6 +15,7 @@
"hark": "ibc/hark#main-with-raf", "hark": "ibc/hark#main-with-raf",
"material-ui": "^0.17.4", "material-ui": "^0.17.4",
"protoo-client": "^1.1.4", "protoo-client": "^1.1.4",
"random-number": "0.0.7",
"random-string": "^0.2.0", "random-string": "^0.2.0",
"react": "^15.5.4", "react": "^15.5.4",
"react-addons-css-transition-group": "^15.5.2", "react-addons-css-transition-group": "^15.5.2",
@ -24,7 +25,8 @@
"react-tap-event-plugin": "^2.0.1", "react-tap-event-plugin": "^2.0.1",
"sdp-transform": "^2.3.0", "sdp-transform": "^2.3.0",
"url-parse": "^1.1.8", "url-parse": "^1.1.8",
"webrtc-adapter": "^3.3.3" "webrtc-adapter": "^3.3.3",
"yaeti": "^1.0.1"
}, },
"devDependencies": { "devDependencies": {
"babel-plugin-transform-object-assign": "^6.22.0", "babel-plugin-transform-object-assign": "^6.22.0",

View File

@ -10,7 +10,7 @@
"colors": "^1.1.2", "colors": "^1.1.2",
"debug": "^2.6.4", "debug": "^2.6.4",
"express": "^4.15.2", "express": "^4.15.2",
"mediasoup": "^1.0.1", "mediasoup": "^1.2.3",
"protoo-server": "^1.1.4" "protoo-server": "^1.1.4"
}, },
"devDependencies": { "devDependencies": {