You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3878 lines
129 KiB
3878 lines
129 KiB
10 months ago
|
/*
|
||
|
* 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 */
|
||
|
const chai = require('chai');
|
||
|
const expect = chai.expect;
|
||
|
const sinon = require('sinon');
|
||
|
chai.use(require('dirty-chai'));
|
||
|
chai.use(require('sinon-chai'));
|
||
|
|
||
|
const mockORTC = require('./ortcmock');
|
||
|
const mockGetUserMedia = require('./gummock');
|
||
|
const shimPeerConnection = require('../rtcpeerconnection');
|
||
|
const SDPUtils = require('sdp');
|
||
|
|
||
|
const FINGERPRINT_SHA256 = '00:00:00:00:00:00:00:00:00:00:00:00:00' +
|
||
|
':00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00';
|
||
|
const ICEUFRAG = 'someufrag';
|
||
|
const ICEPWD = 'somelongpwdwithenoughrandomness';
|
||
|
const SDP_BOILERPLATE = 'v=0\r\n' +
|
||
|
'o=- 166855176514521964 2 IN IP4 127.0.0.1\r\n' +
|
||
|
's=-\r\n' +
|
||
|
't=0 0\r\n' +
|
||
|
'a=msid-semantic:WMS *\r\n';
|
||
|
const MINIMAL_AUDIO_MLINE =
|
||
|
'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' +
|
||
|
'c=IN IP4 0.0.0.0\r\n' +
|
||
|
'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
|
||
|
'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
|
||
|
'a=ice-pwd:' + ICEPWD + '\r\n' +
|
||
|
'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
|
||
|
'a=setup:actpass\r\n' +
|
||
|
'a=mid:audio1\r\n' +
|
||
|
'a=sendonly\r\n' +
|
||
|
'a=rtcp-mux\r\n' +
|
||
|
'a=rtcp-rsize\r\n' +
|
||
|
'a=rtpmap:111 opus/48000/2\r\n' +
|
||
|
'a=ssrc:1001 cname:some\r\n';
|
||
|
|
||
|
// this detects that we are not running in a browser.
|
||
|
const mockWindow = typeof window === 'undefined';
|
||
|
|
||
|
describe('Edge shim', () => {
|
||
|
let RTCPeerConnection;
|
||
|
beforeEach(() => {
|
||
|
if (mockWindow) {
|
||
|
global.window = {setTimeout};
|
||
|
mockGetUserMedia(window);
|
||
|
mockORTC(window);
|
||
|
global.navigator = window.navigator;
|
||
|
}
|
||
|
RTCPeerConnection = shimPeerConnection(window, 15025);
|
||
|
});
|
||
|
|
||
|
beforeEach(() => {
|
||
|
let streams = [];
|
||
|
let release = () => {
|
||
|
streams.forEach((stream) => {
|
||
|
stream.getTracks().forEach((track) => {
|
||
|
track.stop();
|
||
|
});
|
||
|
});
|
||
|
streams = [];
|
||
|
};
|
||
|
|
||
|
let origGetUserMedia = navigator.getUserMedia.bind(navigator);
|
||
|
navigator.getUserMedia = (constraints, cb, eb) => {
|
||
|
origGetUserMedia(constraints, (stream) => {
|
||
|
streams.push(stream);
|
||
|
if (cb) {
|
||
|
cb.apply(null, [stream]);
|
||
|
}
|
||
|
}, eb);
|
||
|
};
|
||
|
navigator.getUserMedia.restore = () => {
|
||
|
navigator.getUserMedia = origGetUserMedia;
|
||
|
release();
|
||
|
};
|
||
|
|
||
|
let origMediaDevicesGetUserMedia =
|
||
|
navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
|
||
|
navigator.mediaDevices.getUserMedia = (constraints) => {
|
||
|
return origMediaDevicesGetUserMedia(constraints, (stream) => {
|
||
|
streams.push(stream);
|
||
|
return stream;
|
||
|
});
|
||
|
};
|
||
|
navigator.mediaDevices.getUserMedia.restore = () => {
|
||
|
navigator.mediaDevices.getUserMedia = origMediaDevicesGetUserMedia;
|
||
|
release();
|
||
|
};
|
||
|
});
|
||
|
|
||
|
afterEach(() => {
|
||
|
navigator.getUserMedia.restore();
|
||
|
navigator.mediaDevices.getUserMedia.restore();
|
||
|
});
|
||
|
|
||
|
describe('RTCPeerConnection constructor', () => {
|
||
|
it('throws a NotSupportedError when called with ' +
|
||
|
'rtcpMuxPolicy negotiate', () => {
|
||
|
const constructor = () => {
|
||
|
return new RTCPeerConnection({rtcpMuxPolicy: 'negotiate'});
|
||
|
};
|
||
|
expect(constructor).to.throw(/rtcpMuxPolicy/)
|
||
|
.that.has.property('name').that.equals('NotSupportedError');
|
||
|
});
|
||
|
|
||
|
describe('when RTCIceCandidatePoolSize is set', () => {
|
||
|
beforeEach(() => {
|
||
|
sinon.spy(window, 'RTCIceGatherer');
|
||
|
});
|
||
|
|
||
|
afterEach(() => {
|
||
|
window.RTCIceGatherer.restore();
|
||
|
});
|
||
|
|
||
|
it('creates an ICE Gatherer', () => {
|
||
|
new RTCPeerConnection({iceCandidatePoolSize: 1});
|
||
|
expect(window.RTCIceGatherer).to.have.been.calledOnce();
|
||
|
});
|
||
|
|
||
|
// TODO: those tests are convenient because they are sync and
|
||
|
// dont require createOffer-SLD before creating the gatherer.
|
||
|
it('sets default ICETransportPolicy on RTCIceGatherer', () => {
|
||
|
new RTCPeerConnection({iceCandidatePoolSize: 1});
|
||
|
expect(window.RTCIceGatherer).to.have.been.calledWith(sinon.match({
|
||
|
gatherPolicy: 'all'
|
||
|
}));
|
||
|
});
|
||
|
|
||
|
it('sets ICETransportPolicy=all on RTCIceGatherer', () => {
|
||
|
new RTCPeerConnection({iceCandidatePoolSize: 1,
|
||
|
iceTransportPolicy: 'all'});
|
||
|
expect(window.RTCIceGatherer).to.have.been.calledWith(sinon.match({
|
||
|
gatherPolicy: 'all'
|
||
|
}));
|
||
|
});
|
||
|
it('sets ICETransportPolicy=relay on RTCIceGatherer', () => {
|
||
|
new RTCPeerConnection({iceCandidatePoolSize: 1,
|
||
|
iceTransportPolicy: 'relay'});
|
||
|
expect(window.RTCIceGatherer).to.have.been.calledWith(sinon.match({
|
||
|
gatherPolicy: 'relay'
|
||
|
}));
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('prototype', () => {
|
||
|
['icecandidate', 'addstream', 'removestream', 'track',
|
||
|
'signalingstatechange', 'iceconnectionstatechange',
|
||
|
'icegatheringstatechange', 'negotiationneeded'].forEach((name) => {
|
||
|
it('has on' + name + ' handler', () => {
|
||
|
expect(RTCPeerConnection.prototype)
|
||
|
.to.have.ownPropertyDescriptor('on' + name);
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('close', () => {
|
||
|
let pc;
|
||
|
beforeEach(() => {
|
||
|
pc = new RTCPeerConnection();
|
||
|
});
|
||
|
it('sets the signalingState to closed', () => {
|
||
|
pc.close();
|
||
|
expect(pc.signalingState).to.equal('closed');
|
||
|
});
|
||
|
|
||
|
it('does not fire signalingstatechange', () => {
|
||
|
pc.onsignalingstatechange = sinon.stub();
|
||
|
pc.close();
|
||
|
expect(pc.onsignalingstatechange).not.to.have.been.calledWith();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('setLocalDescription', () => {
|
||
|
let pc;
|
||
|
beforeEach(() => {
|
||
|
pc = new RTCPeerConnection();
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
if (pc.signalingState !== 'closed') {
|
||
|
pc.close();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
it('returns a promise', (done) => {
|
||
|
pc.createOffer({offerToReceiveAudio: true})
|
||
|
.then((offer) => {
|
||
|
return pc.setLocalDescription(offer);
|
||
|
})
|
||
|
.then(done);
|
||
|
});
|
||
|
|
||
|
it('calls the legacy success callback', (done) => {
|
||
|
pc.createOffer({offerToReceiveAudio: true})
|
||
|
.then((offer) => {
|
||
|
return pc.setLocalDescription(offer, done, () => {});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('throws an InvalidStateError when called after close', (done) => {
|
||
|
pc.createOffer({offerToReceiveAudio: true})
|
||
|
.then((offer) => {
|
||
|
pc.close();
|
||
|
return pc.setLocalDescription(offer);
|
||
|
})
|
||
|
.catch((e) => {
|
||
|
expect(e.name).to.equal('InvalidStateError');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
it('throws an InvalidStateError when called after close ' +
|
||
|
'(callback)', (done) => {
|
||
|
pc.createOffer({offerToReceiveAudio: true})
|
||
|
.then((offer) => {
|
||
|
pc.close();
|
||
|
return pc.setLocalDescription(offer, undefined, (e) => {
|
||
|
expect(e.name).to.equal('InvalidStateError');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('throws a TypeError when called with an ' +
|
||
|
'unsupported description type', (done) => {
|
||
|
pc.setLocalDescription({type: 'invalid'})
|
||
|
.catch((e) => {
|
||
|
expect(e.name).to.equal('TypeError');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
|
||
|
it('changes the signalingState to have-local-offer', (done) => {
|
||
|
pc.createOffer({offerToReceiveAudio: true})
|
||
|
.then((offer) => {
|
||
|
return pc.setLocalDescription(offer);
|
||
|
})
|
||
|
.then(() => {
|
||
|
expect(pc.localDescription.type).to.equal('offer');
|
||
|
expect(pc.signalingState = 'have-local-offer');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('calls the signalingstatechange event', () => {
|
||
|
pc.onsignalingstatechange = sinon.stub();
|
||
|
pc.createOffer({offerToReceiveAudio: true})
|
||
|
.then((offer) => {
|
||
|
return pc.setLocalDescription(offer);
|
||
|
})
|
||
|
.then(() => {
|
||
|
expect(pc.onsignalingstatechange).to.have.been.calledOnce();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('InvalidStateError is thrown when called with', () => {
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
|
||
|
it('an offer in signalingState have-remote-offer', (done) => {
|
||
|
pc.setRemoteDescription({type: 'offer', sdp})
|
||
|
.then(() => {
|
||
|
return pc.setLocalDescription({type: 'offer'});
|
||
|
})
|
||
|
.catch((e) => {
|
||
|
expect(e.name).to.equal('InvalidStateError');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('an answer in signalingState have-local-offer', (done) => {
|
||
|
pc.createOffer({offerToReceiveAudio: true})
|
||
|
.then((offer) => {
|
||
|
return pc.setLocalDescription(offer);
|
||
|
})
|
||
|
.then(() => {
|
||
|
return pc.setLocalDescription({type: 'answer'});
|
||
|
})
|
||
|
.catch((e) => {
|
||
|
expect(e.name).to.equal('InvalidStateError');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('starts emitting ICE candidates', () => {
|
||
|
let clock;
|
||
|
beforeEach(() => {
|
||
|
clock = sinon.useFakeTimers();
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
clock.restore();
|
||
|
});
|
||
|
|
||
|
describe('calls', () => {
|
||
|
it('the onicecandidate callback', (done) => {
|
||
|
pc.onicegatheringstatechange = () => {
|
||
|
if (pc.iceGatheringState === 'complete') {
|
||
|
expect(pc.onicecandidate).to.have.been.calledWith();
|
||
|
done();
|
||
|
}
|
||
|
};
|
||
|
pc.onicecandidate = sinon.stub();
|
||
|
pc.createOffer({offerToReceiveAudio: true})
|
||
|
.then(offer => pc.setLocalDescription(offer))
|
||
|
.then(() => {
|
||
|
window.setTimeout(() => {
|
||
|
clock.tick(500);
|
||
|
});
|
||
|
clock.tick(0);
|
||
|
});
|
||
|
});
|
||
|
it('the icecandidate event listener', (done) => {
|
||
|
const stub = sinon.stub();
|
||
|
pc.onicegatheringstatechange = () => {
|
||
|
if (pc.iceGatheringState === 'complete') {
|
||
|
expect(stub).to.have.been.calledWith();
|
||
|
done();
|
||
|
}
|
||
|
};
|
||
|
pc.addEventListener('icecandidate', stub);
|
||
|
pc.createOffer({offerToReceiveAudio: true})
|
||
|
.then(offer => pc.setLocalDescription(offer))
|
||
|
.then(() => {
|
||
|
window.setTimeout(() => {
|
||
|
clock.tick(500);
|
||
|
});
|
||
|
clock.tick(0);
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('updates localDescription.sdp with candidates', (done) => {
|
||
|
pc.onicegatheringstatechange = () => {
|
||
|
if (pc.iceGatheringState === 'complete') {
|
||
|
expect(SDPUtils.matchPrefix(pc.localDescription.sdp,
|
||
|
'a=candidate:').length).to.be.above(0);
|
||
|
expect(SDPUtils.matchPrefix(pc.localDescription.sdp,
|
||
|
'a=end-of-candidates')).to.have.length(1);
|
||
|
done();
|
||
|
}
|
||
|
};
|
||
|
pc.createOffer({offerToReceiveAudio: true})
|
||
|
.then(offer => pc.setLocalDescription(offer))
|
||
|
.then(() => {
|
||
|
window.setTimeout(() => {
|
||
|
clock.tick(500);
|
||
|
});
|
||
|
clock.tick(0);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('changes iceGatheringState and emits icegatheringstatechange ' +
|
||
|
'event', (done) => {
|
||
|
let states = [];
|
||
|
pc.addEventListener('icegatheringstatechange', () => {
|
||
|
states.push(pc.iceGatheringState);
|
||
|
if (pc.iceGatheringState === 'complete') {
|
||
|
expect(states.length).to.equal(2);
|
||
|
expect(states).to.contain('gathering');
|
||
|
expect(states).to.contain('complete');
|
||
|
done();
|
||
|
}
|
||
|
});
|
||
|
pc.createOffer({offerToReceiveAudio: true})
|
||
|
.then(offer => pc.setLocalDescription(offer))
|
||
|
.then(() => {
|
||
|
expect(pc.iceGatheringState).to.equal('new');
|
||
|
clock.tick(500);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('does not serialize extra parameters in ' +
|
||
|
'RTCICECandidate.toJSON', (done) => {
|
||
|
const candidates = [];
|
||
|
pc.onicecandidate = (e) => {
|
||
|
candidates.push(e.candidate);
|
||
|
};
|
||
|
pc.onicegatheringstatechange = () => {
|
||
|
if (pc.iceGatheringState === 'complete') {
|
||
|
const reserialized = JSON.parse(JSON.stringify(candidates[0]));
|
||
|
expect(reserialized.candidate).to.be.a('string');
|
||
|
expect(reserialized.usernameFragment).to.be.a('string');
|
||
|
expect(reserialized.sdpMid).to.be.a('string');
|
||
|
expect(reserialized.sdpMLineIndex).to.equal(0);
|
||
|
expect(Object.keys(reserialized)).to.have.length(4);
|
||
|
done();
|
||
|
}
|
||
|
};
|
||
|
pc.createOffer({offerToReceiveAudio: true})
|
||
|
.then(offer => pc.setLocalDescription(offer))
|
||
|
.then(() => {
|
||
|
window.setTimeout(() => {
|
||
|
clock.tick(500);
|
||
|
});
|
||
|
clock.tick(0);
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('after setRemoteDescription', () => {
|
||
|
beforeEach(() => {
|
||
|
sinon.spy(window.RTCIceTransport.prototype, 'start');
|
||
|
sinon.spy(window.RTCDtlsTransport.prototype, 'start');
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
window.RTCIceTransport.prototype.start.restore();
|
||
|
window.RTCDtlsTransport.prototype.start.restore();
|
||
|
});
|
||
|
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
|
||
|
it('starts the ice transport', (done) => {
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
return pc.setLocalDescription(answer);
|
||
|
})
|
||
|
.then(() => {
|
||
|
const receiver = pc.getReceivers()[0];
|
||
|
const iceTransport = receiver.transport.transport;
|
||
|
expect(iceTransport.start).to.have.been.calledOnce();
|
||
|
expect(iceTransport.start).to.have.been.calledWith(
|
||
|
sinon.match.any,
|
||
|
sinon.match({
|
||
|
usernameFragment: '' + ICEUFRAG + '',
|
||
|
password: '' + ICEPWD + ''
|
||
|
})
|
||
|
);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('starts the dtls transport', (done) => {
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
return pc.setLocalDescription(answer);
|
||
|
})
|
||
|
.then(() => {
|
||
|
const receiver = pc.getReceivers()[0];
|
||
|
const dtlsTransport = receiver.transport;
|
||
|
expect(dtlsTransport.start).to.have.been.calledOnce();
|
||
|
expect(dtlsTransport.start).to.have.been.calledWith(
|
||
|
sinon.match({
|
||
|
role: 'auto',
|
||
|
fingerprints: sinon.match([
|
||
|
sinon.match({
|
||
|
algorithm: 'sha-256',
|
||
|
value: FINGERPRINT_SHA256
|
||
|
})
|
||
|
])
|
||
|
})
|
||
|
);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('setRemoteDescription', () => {
|
||
|
let pc;
|
||
|
beforeEach(() => {
|
||
|
pc = new RTCPeerConnection();
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
if (pc.signalingState !== 'closed') {
|
||
|
pc.close();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
it('returns a promise', (done) => {
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(done);
|
||
|
});
|
||
|
it('calls the legacy success callback', (done) => {
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp}, done, () => {});
|
||
|
});
|
||
|
|
||
|
it('changes the signalingState to have-remote-offer', (done) => {
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
expect(pc.signalingState = 'have-remote-offer');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('calls the signalingstatechange event', () => {
|
||
|
pc.onsignalingstatechange = sinon.stub();
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
expect(pc.onsignalingstatechange).to.have.been.calledOnce();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('throws an InvalidStateError when called after close', (done) => {
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
|
||
|
pc.close();
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.catch((e) => {
|
||
|
expect(e.name).to.equal('InvalidStateError');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('throws an InvalidStateError when called after close ' +
|
||
|
'(callback)', (done) => {
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
|
||
|
pc.close();
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp}, undefined, (e) => {
|
||
|
expect(e.name).to.equal('InvalidStateError');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('throws a TypeError when called with an ' +
|
||
|
'unsupported description type', (done) => {
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
|
||
|
pc.setRemoteDescription({type: 'invalid', sdp: sdp})
|
||
|
.catch((e) => {
|
||
|
expect(e.name).to.equal('TypeError');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('sets the remoteDescription', (done) => {
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp}, () => {
|
||
|
expect(pc.remoteDescription.type).to.equal('offer');
|
||
|
expect(pc.remoteDescription.sdp).to.equal(sdp);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when called with an offer containing a track', () => {
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE +
|
||
|
'a=ssrc:1001 msid:stream1 track1\r\n' +
|
||
|
it('triggers onaddstream', (done) => {
|
||
|
pc.onaddstream = function(event) {
|
||
|
const stream = event.stream;
|
||
|
expect(stream.getTracks().length).to.equal(1);
|
||
|
expect(stream.getTracks()[0].kind).to.equal('audio');
|
||
|
|
||
|
done();
|
||
|
};
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp});
|
||
|
});
|
||
|
|
||
|
it('emits a addstream event', (done) => {
|
||
|
pc.addEventListener('addstream', function(event) {
|
||
|
const stream = event.stream;
|
||
|
expect(stream.getTracks().length).to.equal(1);
|
||
|
expect(stream.getTracks()[0].kind).to.equal('audio');
|
||
|
|
||
|
done();
|
||
|
});
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp});
|
||
|
});
|
||
|
|
||
|
it('triggers ontrack', (done) => {
|
||
|
pc.ontrack = function(event) {
|
||
|
expect(event.track.kind).to.equal('audio');
|
||
|
expect(event.receiver);
|
||
|
expect(event.streams.length).to.equal(1);
|
||
|
|
||
|
done();
|
||
|
};
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp});
|
||
|
});
|
||
|
|
||
|
it('emits a track event', (done) => {
|
||
|
pc.addEventListener('track', function(event) {
|
||
|
expect(event.track.kind).to.equal('audio');
|
||
|
expect(event.receiver);
|
||
|
expect(event.streams.length).to.equal(1);
|
||
|
|
||
|
done();
|
||
|
});
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp});
|
||
|
});
|
||
|
|
||
|
it('triggers ontrack and track event before resolving', (done) => {
|
||
|
let clock = sinon.useFakeTimers();
|
||
|
var trackEvent = sinon.stub();
|
||
|
pc.addEventListener('track', trackEvent);
|
||
|
pc.ontrack = sinon.stub();
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
window.setTimeout(() => {
|
||
|
expect(trackEvent).to.have.been.calledWith();
|
||
|
expect(pc.ontrack).to.have.been.calledWith();
|
||
|
clock.restore();
|
||
|
done();
|
||
|
}, 0);
|
||
|
clock.tick(500);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('without a stream (stream id -)', () => {
|
||
|
it('does not trigger onaddstream', (done) => {
|
||
|
let clock = sinon.useFakeTimers();
|
||
|
pc.onaddstream = sinon.stub();
|
||
|
pc.setRemoteDescription({type: 'offer',
|
||
|
sdp: sdp.replace('stream1', '-')})
|
||
|
.then(() => {
|
||
|
window.setTimeout(() => {
|
||
|
expect(pc.onaddstream).not.to.have.been.calledWith();
|
||
|
clock.restore();
|
||
|
done();
|
||
|
}, 0);
|
||
|
clock.tick(500);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('does trigger ontrack with an empty streams set', (done) => {
|
||
|
pc.addEventListener('track', function(event) {
|
||
|
expect(event.track.kind).to.equal('audio');
|
||
|
expect(event.receiver);
|
||
|
expect(event.streams.length).to.equal(0);
|
||
|
|
||
|
done();
|
||
|
});
|
||
|
pc.setRemoteDescription({type: 'offer',
|
||
|
sdp: sdp.replace('stream1', '-')});
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when called with an offer without (explicit) tracks', () => {
|
||
|
const sdp = (SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE)
|
||
|
.replace('a=msid-semantics:WMS *\r\n', '');
|
||
|
|
||
|
it('triggers onaddstream', (done) => {
|
||
|
pc.onaddstream = function(event) {
|
||
|
const stream = event.stream;
|
||
|
expect(stream.getTracks().length).to.equal(1);
|
||
|
expect(stream.getTracks()[0].kind).to.equal('audio');
|
||
|
|
||
|
done();
|
||
|
};
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp});
|
||
|
});
|
||
|
|
||
|
it('triggers ontrack', (done) => {
|
||
|
pc.ontrack = function(event) {
|
||
|
expect(event.track.kind).to.equal('audio');
|
||
|
expect(event).to.have.property('receiver');
|
||
|
expect(event).to.have.property('transceiver');
|
||
|
expect(event.streams).to.have.lengthOf(1);
|
||
|
done();
|
||
|
};
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when called with an offer containing multiple streams ' +
|
||
|
'/ tracks', () => {
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE +
|
||
|
'a=ssrc:1001 msid:stream1 track1\r\n' +
|
||
|
'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' +
|
||
|
'c=IN IP4 0.0.0.0\r\n' +
|
||
|
'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
|
||
|
'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
|
||
|
'a=ice-pwd:' + ICEPWD + '\r\n' +
|
||
|
'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
|
||
|
'a=setup:actpass\r\n' +
|
||
|
'a=mid:audio1\r\n' +
|
||
|
'a=sendonly\r\n' +
|
||
|
'a=rtcp-mux\r\n' +
|
||
|
'a=rtcp-rsize\r\n' +
|
||
|
'a=rtpmap:111 opus/48000/2\r\n' +
|
||
|
'a=ssrc:2002 msid:stream2 track2\r\n' +
|
||
|
'a=ssrc:2002 cname:some\r\n';
|
||
|
|
||
|
it('triggers onaddstream twice', (done) => {
|
||
|
let numStreams = 0;
|
||
|
pc.onaddstream = function(event) {
|
||
|
numStreams++;
|
||
|
expect(event.stream.id).to.equal('stream' + numStreams);
|
||
|
if (numStreams === 2) {
|
||
|
done();
|
||
|
}
|
||
|
};
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp});
|
||
|
});
|
||
|
|
||
|
it('triggers ontrack twice', (done) => {
|
||
|
let numTracks = 0;
|
||
|
pc.ontrack = function(event) {
|
||
|
numTracks++;
|
||
|
expect(event.streams[0].id).to.equal('stream' + numTracks);
|
||
|
if (numTracks === 2) {
|
||
|
done();
|
||
|
}
|
||
|
};
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when called with a bundle offer after adding ' +
|
||
|
'two tracks', () => {
|
||
|
const sdp = SDP_BOILERPLATE +
|
||
|
'a=group:BUNDLE audio1 video1\r\n' +
|
||
|
MINIMAL_AUDIO_MLINE +
|
||
|
'a=ssrc:1001 msid:stream1 track1\r\n' +
|
||
|
'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' +
|
||
|
'c=IN IP4 0.0.0.0\r\n' +
|
||
|
'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
|
||
|
'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
|
||
|
'a=ice-pwd:' + ICEPWD + '\r\n' +
|
||
|
'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
|
||
|
'a=setup:actpass\r\n' +
|
||
|
'a=mid:audio1\r\n' +
|
||
|
'a=sendonly\r\n' +
|
||
|
'a=rtcp-mux\r\n' +
|
||
|
'a=rtcp-rsize\r\n' +
|
||
|
'a=rtpmap:111 opus/48000/2\r\n' +
|
||
|
'a=ssrc:2002 msid:stream2 track2\r\n' +
|
||
|
'a=ssrc:2002 cname:some\r\n';
|
||
|
it('disposes the second ice transport', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({audio: true, video: true})
|
||
|
.then((stream) => {
|
||
|
// this creates two transceivers with ice transports.
|
||
|
pc.addStream(stream);
|
||
|
|
||
|
// this has bundle so will set usingBundle. But two
|
||
|
// transceivers and their ice/dtls transports exist
|
||
|
// and the second one needs to be disposed.
|
||
|
return pc.setRemoteDescription({type: 'offer', sdp: sdp});
|
||
|
})
|
||
|
.then(() => {
|
||
|
const senders = pc.getSenders();
|
||
|
// the second ice transport should have been disposed.
|
||
|
expect(senders[0].transport.transport).to
|
||
|
.equal(senders[1].transport.transport);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when called with an offer without an a=ssrc line', () => {
|
||
|
const sdp = SDP_BOILERPLATE +
|
||
|
'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' +
|
||
|
'c=IN IP4 0.0.0.0\r\n' +
|
||
|
'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
|
||
|
'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
|
||
|
'a=ice-pwd:' + ICEPWD + '\r\n' +
|
||
|
'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
|
||
|
'a=setup:actpass\r\n' +
|
||
|
'a=mid:audio1\r\n' +
|
||
|
'a=sendonly\r\n' +
|
||
|
'a=rtcp-mux\r\n' +
|
||
|
'a=rtcp-rsize\r\n' +
|
||
|
'a=rtpmap:111 opus/48000/2\r\n';
|
||
|
beforeEach(() => {
|
||
|
sinon.spy(window.RTCRtpReceiver.prototype, 'receive');
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
window.RTCRtpReceiver.prototype.receive.restore();
|
||
|
});
|
||
|
|
||
|
it('calls RTCRtpReceiver.recv with encodings set to [{}]', () => {
|
||
|
return pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
const receiver = pc.getReceivers()[0];
|
||
|
expect(receiver.receive).to.have.been.calledWith(
|
||
|
sinon.match({encodings: [{}]})
|
||
|
);
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
// TODO: add a test for recvonly to show it doesn't trigger the callback.
|
||
|
// probably easiest done using a sinon.stub
|
||
|
//
|
||
|
describe('sets the canTrickleIceCandidates property', () => {
|
||
|
it('to true when called with an offer that contains ' +
|
||
|
'a=ice-options:trickle', (done) => {
|
||
|
const sdp = SDP_BOILERPLATE +
|
||
|
'a=ice-options:trickle\r\n';
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
expect(pc.canTrickleIceCandidates).to.equal(true);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('to false when called with an offer that does not contain ' +
|
||
|
'a=ice-options:trickle', (done) => {
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
expect(pc.canTrickleIceCandidates).to.equal(false);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when called with an offer containing candidates', () => {
|
||
|
beforeEach(() => {
|
||
|
sinon.spy(window.RTCIceTransport.prototype, 'addRemoteCandidate');
|
||
|
sinon.spy(window.RTCIceTransport.prototype, 'setRemoteCandidates');
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
window.RTCIceTransport.prototype.addRemoteCandidate.restore();
|
||
|
window.RTCIceTransport.prototype.setRemoteCandidates.restore();
|
||
|
});
|
||
|
const candidateString = 'a=candidate:702786350 1 udp 41819902 ' +
|
||
|
'8.8.8.8 60769 typ host';
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE +
|
||
|
'a=ssrc:1001 msid:stream1 track1\r\n' +
|
||
|
candidateString + '\r\n';
|
||
|
it('adds the candidates to the ice transport', (done) => {
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
const receiver = pc.getReceivers()[0];
|
||
|
const iceTransport = receiver.transport.transport;
|
||
|
expect(iceTransport.addRemoteCandidate).to.have.been.calledOnce();
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('interprets end-of-candidates', (done) => {
|
||
|
pc.setRemoteDescription({type: 'offer',
|
||
|
sdp: sdp + 'a=end-of-candidates\r\n'
|
||
|
})
|
||
|
.then(() => {
|
||
|
const receiver = pc.getReceivers()[0];
|
||
|
const iceTransport = receiver.transport.transport;
|
||
|
expect(iceTransport.setRemoteCandidates).to.have.been.calledOnce();
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('does not add the candidate in a subsequent offer ' +
|
||
|
'again', (done) => {
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
// call SRD again.
|
||
|
return pc.setRemoteDescription({type: 'offer', sdp: sdp});
|
||
|
})
|
||
|
.then(() => {
|
||
|
const receiver = pc.getReceivers()[0];
|
||
|
const iceTransport = receiver.transport.transport;
|
||
|
expect(iceTransport.addRemoteCandidate).to.have.been.calledOnce();
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('does not add the candidates when they are also supplied ' +
|
||
|
'with addIceCandidate', (done) => {
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
const receiver = pc.getReceivers()[0];
|
||
|
const iceTransport = receiver.transport.transport;
|
||
|
|
||
|
pc.addIceCandidate({sdpMid: 'audio1', sdpMLineIndex: 0,
|
||
|
candidate: candidateString})
|
||
|
.catch(() => {});
|
||
|
expect(iceTransport.addRemoteCandidate).to.have.been.calledOnce();
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('InvalidStateError is thrown when called with', () => {
|
||
|
it('an answer in signalingState stable', (done) => {
|
||
|
pc.setRemoteDescription({type: 'answer'})
|
||
|
.catch((e) => {
|
||
|
expect(e.name).to.equal('InvalidStateError');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('an offer in signalingState have-local-offer', (done) => {
|
||
|
pc.createOffer({offerToReceiveAudio: true})
|
||
|
.then((offer) => {
|
||
|
return pc.setLocalDescription(offer);
|
||
|
})
|
||
|
.then(() => {
|
||
|
return pc.setRemoteDescription({type: 'offer'});
|
||
|
})
|
||
|
.catch((e) => {
|
||
|
expect(e.name).to.equal('InvalidStateError');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when called with an subsequent offer', () => {
|
||
|
let clock;
|
||
|
beforeEach(() => {
|
||
|
clock = sinon.useFakeTimers();
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
clock.restore();
|
||
|
});
|
||
|
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE +
|
||
|
'a=ssrc:1001 msid:stream1 audiotrack\r\n';
|
||
|
const videoPart =
|
||
|
'm=video 9 UDP/TLS/RTP/SAVPF 102 103\r\n' +
|
||
|
'c=IN IP4 0.0.0.0\r\n' +
|
||
|
'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
|
||
|
'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
|
||
|
'a=ice-pwd:' + ICEPWD + '\r\n' +
|
||
|
'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
|
||
|
'a=setup:actpass\r\n' +
|
||
|
'a=mid:video1\r\n' +
|
||
|
'a=sendrecv\r\n' +
|
||
|
'a=rtcp-mux\r\n' +
|
||
|
'a=rtcp-rsize\r\n' +
|
||
|
'a=rtpmap:102 vp8/90000\r\n' +
|
||
|
'a=rtpmap:103 rtx/90000\r\n' +
|
||
|
'a=fmtp:103 apt=102\r\n' +
|
||
|
'a=ssrc-group:FID 1001 1002\r\n' +
|
||
|
'a=ssrc:1001 msid:stream1 videotrack\r\n' +
|
||
|
'a=ssrc:1001 cname:some\r\n' +
|
||
|
'a=ssrc:1002 msid:stream1 videotrack\r\n' +
|
||
|
'a=ssrc:1002 cname:some\r\n';
|
||
|
|
||
|
describe('adding a new track', () => {
|
||
|
it('triggers ontrack', (done) => {
|
||
|
pc.onaddstream = sinon.stub();
|
||
|
pc.ontrack = sinon.stub();
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
return pc.setRemoteDescription({type: 'offer',
|
||
|
sdp: sdp + videoPart});
|
||
|
})
|
||
|
.then(() => {
|
||
|
window.setTimeout(() => {
|
||
|
expect(pc.onaddstream).to.have.been.calledOnce();
|
||
|
expect(pc.ontrack).to.have.been.calledTwice();
|
||
|
done();
|
||
|
});
|
||
|
clock.tick(500);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('fires the stream addtrack event', (done) => {
|
||
|
let remoteStream;
|
||
|
pc.onaddstream = (e) => {
|
||
|
remoteStream = e.stream;
|
||
|
remoteStream.addEventListener('addtrack', (event) => {
|
||
|
expect(event).to.be.an.instanceOf(window.MediaStreamTrackEvent);
|
||
|
expect(event).to.have.property('track');
|
||
|
expect(event.track.id).to.equal('videotrack');
|
||
|
done();
|
||
|
});
|
||
|
};
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
window.setTimeout(() => {
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp + videoPart});
|
||
|
});
|
||
|
clock.tick(500);
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('removing a track', () => {
|
||
|
it('fires the stream removetrack event', (done) => {
|
||
|
let remoteStream;
|
||
|
pc.onaddstream = (e) => {
|
||
|
remoteStream = e.stream;
|
||
|
remoteStream.addEventListener('removetrack', (event) => {
|
||
|
expect(event).to.be.an.instanceOf(window.MediaStreamTrackEvent);
|
||
|
expect(event).to.have.property('track');
|
||
|
expect(event.track.id).to.equal('videotrack');
|
||
|
done();
|
||
|
});
|
||
|
};
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp + videoPart})
|
||
|
.then(() => {
|
||
|
window.setTimeout(() => {
|
||
|
pc.setRemoteDescription({type: 'offer', sdp:
|
||
|
sdp + videoPart.replace('sendrecv', 'recvonly')});
|
||
|
});
|
||
|
clock.tick(500);
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('going from rejected to non-rejected', () => {
|
||
|
it('triggers ontrack', (done) => {
|
||
|
pc.onaddstream = sinon.stub();
|
||
|
pc.ontrack = sinon.stub();
|
||
|
pc.setRemoteDescription({type: 'offer',
|
||
|
sdp: sdp.replace('m=audio 9', 'm=audio 0')})
|
||
|
.then(() => {
|
||
|
return pc.setRemoteDescription({type: 'offer',
|
||
|
sdp: sdp});
|
||
|
})
|
||
|
.then(() => {
|
||
|
window.setTimeout(() => {
|
||
|
expect(pc.onaddstream).to.have.been.calledOnce();
|
||
|
expect(pc.ontrack).to.have.been.calledOnce();
|
||
|
done();
|
||
|
});
|
||
|
clock.tick(500);
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when rtcp-rsize is', () => {
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE +
|
||
|
'a=ssrc:1001 msid:stream1 track1\r\n';
|
||
|
beforeEach(() => {
|
||
|
sinon.spy(window.RTCRtpReceiver.prototype, 'receive');
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
window.RTCRtpReceiver.prototype.receive.restore();
|
||
|
});
|
||
|
|
||
|
it('set RtpReceiver is called with compound set to false', (done) => {
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
const receiver = pc.getReceivers()[0];
|
||
|
expect(receiver.receive).to.have.been.calledWith(
|
||
|
sinon.match({rtcp: sinon.match({compound: false})})
|
||
|
);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
it('not set RtpReceiver is called with compound set to true', (done) => {
|
||
|
pc.setRemoteDescription({type: 'offer',
|
||
|
sdp: sdp.replace('a=rtcp-rsize\r\n', '')})
|
||
|
.then(() => {
|
||
|
const receiver = pc.getReceivers()[0];
|
||
|
expect(receiver.receive).to.have.been.calledWith(
|
||
|
sinon.match({rtcp: sinon.match({compound: true})})
|
||
|
);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('with an ice-lite offer', () => {
|
||
|
beforeEach(() => {
|
||
|
sinon.spy(window.RTCDtlsTransport.prototype, 'start');
|
||
|
sinon.spy(window.RTCIceTransport.prototype, 'start');
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
window.RTCDtlsTransport.prototype.start.restore();
|
||
|
window.RTCIceTransport.prototype.start.restore();
|
||
|
});
|
||
|
|
||
|
const sdp = SDP_BOILERPLATE +
|
||
|
'a=ice-lite\r\n' +
|
||
|
MINIMAL_AUDIO_MLINE;
|
||
|
|
||
|
it('set the ice role to controlling', (done) => {
|
||
|
pc.setRemoteDescription({type: 'offer', sdp})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
return pc.setLocalDescription(answer);
|
||
|
})
|
||
|
.then(() => {
|
||
|
const receiver = pc.getReceivers()[0];
|
||
|
const dtlsTransport = receiver.transport;
|
||
|
const iceTransport = dtlsTransport.transport;
|
||
|
expect(iceTransport.start).to.have.been.calledOnce();
|
||
|
expect(iceTransport.start).to.have.been.calledWith(
|
||
|
sinon.match.any,
|
||
|
sinon.match.any,
|
||
|
sinon.match('controlling')
|
||
|
);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('sets the dtls role to server', (done) => {
|
||
|
pc.setRemoteDescription({type: 'offer', sdp})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
return pc.setLocalDescription(answer);
|
||
|
})
|
||
|
.then(() => {
|
||
|
const receiver = pc.getReceivers()[0];
|
||
|
const dtlsTransport = receiver.transport;
|
||
|
expect(dtlsTransport.start).to.have.been.calledOnce();
|
||
|
expect(dtlsTransport.start).to.have.been.calledWith(
|
||
|
sinon.match({
|
||
|
role: 'server'
|
||
|
})
|
||
|
);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('with type=answer', () => {
|
||
|
beforeEach(() => {
|
||
|
sinon.spy(window.RTCIceTransport.prototype, 'setRemoteCandidates');
|
||
|
return pc.createOffer({offerToReceiveAudio: true,
|
||
|
offerToReceiveVideo: true})
|
||
|
.then(offer => pc.setLocalDescription(offer));
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
window.RTCIceTransport.prototype.setRemoteCandidates.restore();
|
||
|
});
|
||
|
|
||
|
it('ignores extra candidates in a bundle answer', (done) => {
|
||
|
const sdp = SDP_BOILERPLATE +
|
||
|
'a=group:BUNDLE audio1 video1\r\n' +
|
||
|
MINIMAL_AUDIO_MLINE +
|
||
|
'a=candidate:702786350 1 udp 41819902 8.8.8.8 60769 typ host\r\n' +
|
||
|
'a=end-of-candidates\r\n' +
|
||
|
'm=video 9 UDP/TLS/RTP/SAVPF 102\r\n' +
|
||
|
'c=IN IP4 0.0.0.0\r\n' +
|
||
|
'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
|
||
|
'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
|
||
|
'a=ice-pwd:' + ICEPWD + '\r\n' +
|
||
|
'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
|
||
|
'a=setup:actpass\r\n' +
|
||
|
'a=mid:video1\r\n' +
|
||
|
'a=sendrecv\r\n' +
|
||
|
'a=rtcp-mux\r\n' +
|
||
|
'a=rtcp-rsize\r\n' +
|
||
|
'a=rtpmap:102 vp8/90000\r\n' +
|
||
|
'a=ssrc:1002 cname:some\r\n' +
|
||
|
'a=candidate:702786350 1 udp 41819902 8.8.8.8 60769 typ host\r\n' +
|
||
|
'a=end-of-candidates\r\n';
|
||
|
pc.setRemoteDescription({type: 'answer', sdp})
|
||
|
.then(() => {
|
||
|
const receiver = pc.getReceivers()[0];
|
||
|
const iceTransport = receiver.transport.transport;
|
||
|
expect(iceTransport.setRemoteCandidates).to.have.been.calledOnce();
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('treats bundle-only m-lines as not rejected', (done) => {
|
||
|
const sdp = SDP_BOILERPLATE +
|
||
|
'a=group:BUNDLE audio1 video1\r\n' +
|
||
|
MINIMAL_AUDIO_MLINE +
|
||
|
'a=candidate:702786350 1 udp 41819902 8.8.8.8 60769 typ host\r\n' +
|
||
|
'a=msid:stream1 audiotrack\r\n' +
|
||
|
'a=end-of-candidates\r\n' +
|
||
|
'm=video 0 UDP/TLS/RTP/SAVPF 102\r\n' +
|
||
|
'c=IN IP4 0.0.0.0\r\n' +
|
||
|
'a=bundle-only\r\n' +
|
||
|
'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
|
||
|
'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
|
||
|
'a=ice-pwd:' + ICEPWD + '\r\n' +
|
||
|
'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
|
||
|
'a=setup:actpass\r\n' +
|
||
|
'a=mid:video1\r\n' +
|
||
|
'a=sendrecv\r\n' +
|
||
|
'a=rtcp-mux\r\n' +
|
||
|
'a=rtcp-rsize\r\n' +
|
||
|
'a=rtpmap:102 vp8/90000\r\n' +
|
||
|
'a=ssrc:1002 cname:some\r\n' +
|
||
|
'a=candidate:702786350 1 udp 41819902 8.8.8.8 60769 typ host\r\n' +
|
||
|
'a=msid:stream1 videotrack\r\n' +
|
||
|
'a=end-of-candidates\r\n';
|
||
|
pc.setRemoteDescription({type: 'offer', sdp})
|
||
|
.then(() => {
|
||
|
const receivers = pc.getReceivers();
|
||
|
expect(receivers).to.have.length(2);
|
||
|
expect(receivers[1].track.id).to.equal('videotrack');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('createOffer', () => {
|
||
|
let pc;
|
||
|
beforeEach(() => {
|
||
|
pc = new RTCPeerConnection();
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
if (pc.signalingState !== 'closed') {
|
||
|
pc.close();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
it('returns a promise', (done) => {
|
||
|
pc.createOffer({offerToReceiveAudio: true})
|
||
|
.then(() => {
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('calls the legacy success callback', (done) => {
|
||
|
pc.createOffer((offer) => {
|
||
|
expect(offer.type).to.equal('offer');
|
||
|
done();
|
||
|
}, () => {}, {offerToReceiveAudio: true});
|
||
|
});
|
||
|
|
||
|
it('calls the legacy success callback and resolves with ' +
|
||
|
'no arguments', (done) => {
|
||
|
pc.createOffer((offer) => {})
|
||
|
.then((shouldBeUndefined) => {
|
||
|
expect(shouldBeUndefined).to.equal(undefined);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('does not change the signalingState', (done) => {
|
||
|
pc.createOffer({offerToReceiveAudio: true})
|
||
|
.then(() => {
|
||
|
expect(pc.signalingState).to.equal('stable');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('uses pooled RTCIceGatherer', (done) => {
|
||
|
pc.close();
|
||
|
pc = new RTCPeerConnection({iceCandidatePoolSize: 1});
|
||
|
pc.createOffer({offerToReceiveAudio: true})
|
||
|
.then(() => {
|
||
|
expect(pc._iceGatherers).to.have.length(0);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('does not start emitting ICE candidates', (done) => {
|
||
|
let clock = sinon.useFakeTimers();
|
||
|
pc.onicecandidate = sinon.stub();
|
||
|
pc.createOffer({offerToReceiveAudio: true})
|
||
|
.then(() => {
|
||
|
clock.tick(500);
|
||
|
expect(pc.onicecandidate).not.to.have.been.calledWith();
|
||
|
clock.restore();
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('throws an InvalidStateError when called after close', (done) => {
|
||
|
pc.close();
|
||
|
pc.createOffer({offerToReceiveAudio: true})
|
||
|
.catch((e) => {
|
||
|
expect(e.name).to.equal('InvalidStateError');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('throws an InvalidStateError when called after close ' +
|
||
|
'(callback)', (done) => {
|
||
|
pc.close();
|
||
|
pc.createOffer(undefined, (e) => {
|
||
|
expect(e.name).to.equal('InvalidStateError');
|
||
|
done();
|
||
|
}, {offerToReceiveAudio: true});
|
||
|
});
|
||
|
|
||
|
describe('throws a TypeError when called with legacy constraints', () => {
|
||
|
it('(optional)', () => {
|
||
|
expect(() => pc.createOffer({optional: {OfferToReceiveAudio: true}}))
|
||
|
.to.throw()
|
||
|
.that.has.property('name').that.equals('TypeError');
|
||
|
});
|
||
|
|
||
|
it('(mandatory)', () => {
|
||
|
expect(() => pc.createOffer({mandatory: {OfferToReceiveAudio: true}}))
|
||
|
.to.throw()
|
||
|
.that.has.property('name').that.equals('TypeError');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when called with offerToReceiveAudio', () => {
|
||
|
it('= true the generated SDP should contain one audio m-line', (done) => {
|
||
|
pc.createOffer({offerToReceiveAudio: true})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections.length).to.equal(1);
|
||
|
expect(SDPUtils.getDirection(sections[0])).to.equal('recvonly');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
// probably legacy which was covered by the spec at some point.
|
||
|
it('= 2 the generated SDP should contain two audio m-lines', (done) => {
|
||
|
pc.createOffer({offerToReceiveAudio: 2})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections.length).to.equal(2);
|
||
|
expect(SDPUtils.getDirection(sections[0])).to.equal('recvonly');
|
||
|
expect(SDPUtils.getDirection(sections[1])).to.equal('recvonly');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('= true the generated SDP should contain one audio m-line', (done) => {
|
||
|
pc.createOffer({offerToReceiveAudio: true})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections.length).to.equal(1);
|
||
|
expect(SDPUtils.getDirection(sections[0])).to.equal('recvonly');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('= false the generated SDP should not offer to receive ' +
|
||
|
'audio', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({audio: true})
|
||
|
.then((stream) => {
|
||
|
pc.addStream(stream);
|
||
|
return pc.createOffer({offerToReceiveAudio: false});
|
||
|
})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections.length).to.equal(1);
|
||
|
expect(SDPUtils.getDirection(sections[0])).to.equal('sendonly');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('= false and no local track the generated SDP should not ' +
|
||
|
'contain a m-line', (done) => {
|
||
|
// see https://github.com/rtcweb-wg/jsep/issues/832
|
||
|
pc.createOffer({offerToReceiveAudio: false})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections).to.have.length(0);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when called with offerToReceiveVideo', () => {
|
||
|
it('= true the generated SDP should contain one video m-line', (done) => {
|
||
|
pc.createOffer({offerToReceiveVideo: true})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections.length).to.equal(1);
|
||
|
expect(SDPUtils.getDirection(sections[0])).to.equal('recvonly');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
// probably legacy which was covered by the spec at some point.
|
||
|
it('= 2 the generated SDP should contain two video m-lines', (done) => {
|
||
|
pc.createOffer({offerToReceiveVideo: 2})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections.length).to.equal(2);
|
||
|
expect(SDPUtils.getDirection(sections[0])).to.equal('recvonly');
|
||
|
expect(SDPUtils.getDirection(sections[1])).to.equal('recvonly');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('= true the generated SDP should contain one video m-line', (done) => {
|
||
|
pc.createOffer({offerToReceiveVideo: true})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections.length).to.equal(1);
|
||
|
expect(SDPUtils.getDirection(sections[0])).to.equal('recvonly');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('= false the generated SDP should not offer to receive ' +
|
||
|
'video', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({video: true})
|
||
|
.then((stream) => {
|
||
|
pc.addStream(stream);
|
||
|
return pc.createOffer({offerToReceiveVideo: false});
|
||
|
})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections.length).to.equal(1);
|
||
|
expect(SDPUtils.getDirection(sections[0])).to.equal('sendonly');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('= false and no local track the generated SDP should not ' +
|
||
|
'contain a m-line', (done) => {
|
||
|
// see https://github.com/rtcweb-wg/jsep/issues/832
|
||
|
pc.createOffer({offerToReceiveVideo: false})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections).to.have.length(0);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when called with offerToReceiveAudio and ' +
|
||
|
'offerToReceiveVideo', () => {
|
||
|
it('the generated SDP should contain two m-lines', (done) => {
|
||
|
pc.createOffer({offerToReceiveAudio: true, offerToReceiveVideo: true})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections.length).to.equal(2);
|
||
|
expect(SDPUtils.getDirection(sections[0])).to.equal('recvonly');
|
||
|
expect(SDPUtils.getKind(sections[0])).to.equal('audio');
|
||
|
expect(SDPUtils.getDirection(sections[1])).to.equal('recvonly');
|
||
|
expect(SDPUtils.getKind(sections[1])).to.equal('video');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when called after adding a stream', () => {
|
||
|
describe('with an audio track', () => {
|
||
|
it('the generated SDP should contain an audio m-line', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({audio: true})
|
||
|
.then((stream) => {
|
||
|
pc.addStream(stream);
|
||
|
return pc.createOffer();
|
||
|
})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections.length).to.equal(1);
|
||
|
expect(SDPUtils.getDirection(sections[0])).to.equal('sendrecv');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('with an audio track not offering to receive audio', () => {
|
||
|
it('the generated SDP should contain a sendonly audio ' +
|
||
|
'm-line', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({audio: true})
|
||
|
.then((stream) => {
|
||
|
pc.addStream(stream);
|
||
|
return pc.createOffer({offerToReceiveAudio: false});
|
||
|
})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections.length).to.equal(1);
|
||
|
expect(SDPUtils.getDirection(sections[0])).to.equal('sendonly');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('with an audio track and offering to receive video', () => {
|
||
|
it('the generated SDP should contain a recvonly m-line', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({audio: true})
|
||
|
.then((stream) => {
|
||
|
pc.addStream(stream);
|
||
|
return pc.createOffer({offerToReceiveVideo: true});
|
||
|
})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections.length).to.equal(2);
|
||
|
expect(SDPUtils.getKind(sections[0])).to.equal('audio');
|
||
|
expect(SDPUtils.getDirection(sections[0])).to.equal('sendrecv');
|
||
|
expect(SDPUtils.getKind(sections[1])).to.equal('video');
|
||
|
expect(SDPUtils.getDirection(sections[1])).to.equal('recvonly');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('with a video track', () => {
|
||
|
it('the generated SDP should contain an video m-line', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({video: true})
|
||
|
.then((stream) => {
|
||
|
pc.addStream(stream);
|
||
|
return pc.createOffer();
|
||
|
})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections.length).to.equal(1);
|
||
|
expect(SDPUtils.getKind(sections[0])).to.equal('video');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('with a video track and offerToReceiveAudio', () => {
|
||
|
it('the generated SDP should contain a video and an ' +
|
||
|
'audio m-line', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({video: true})
|
||
|
.then((stream) => {
|
||
|
pc.addStream(stream);
|
||
|
return pc.createOffer({offerToReceiveAudio: true});
|
||
|
})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections.length).to.equal(2);
|
||
|
expect(SDPUtils.getKind(sections[0])).to.equal('video');
|
||
|
expect(SDPUtils.getKind(sections[1])).to.equal('audio');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
|
||
|
describe('with an audio track and a video track', () => {
|
||
|
it('the generated SDP should contain an audio and video ' +
|
||
|
'm-line', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({audio: true, video: true})
|
||
|
.then((stream) => {
|
||
|
pc.addStream(stream);
|
||
|
return pc.createOffer();
|
||
|
})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections.length).to.equal(2);
|
||
|
expect(SDPUtils.getKind(sections[0])).to.equal('audio');
|
||
|
expect(SDPUtils.getKind(sections[1])).to.equal('video');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('with an audio track and two video tracks', () => {
|
||
|
it('the generated SDP should contain an audio and ' +
|
||
|
'video m-line', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({audio: true, video: true})
|
||
|
.then((stream) => {
|
||
|
pc.addStream(stream);
|
||
|
return navigator.mediaDevices.getUserMedia({video: true});
|
||
|
})
|
||
|
.then((stream) => {
|
||
|
pc.addStream(stream);
|
||
|
return pc.createOffer();
|
||
|
})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections.length).to.equal(3);
|
||
|
expect(SDPUtils.getKind(sections[0])).to.equal('audio');
|
||
|
expect(SDPUtils.getKind(sections[1])).to.equal('video');
|
||
|
expect(SDPUtils.getKind(sections[2])).to.equal('video');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when called after addTrack', () => {
|
||
|
describe('with an audio track', () => {
|
||
|
it('the generated SDP should contain a sendrecv ' +
|
||
|
'audio m-line', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({audio: true})
|
||
|
.then((stream) => {
|
||
|
pc.addTrack(stream.getAudioTracks()[0], stream);
|
||
|
return pc.createOffer();
|
||
|
})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections.length).to.equal(1);
|
||
|
expect(SDPUtils.getDirection(sections[0])).to.equal('sendrecv');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('with an audio track not offering to receive audio', () => {
|
||
|
it('the generated SDP should contain a sendonly audio ' +
|
||
|
'm-line', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({audio: true})
|
||
|
.then((stream) => {
|
||
|
pc.addTrack(stream.getAudioTracks()[0], stream);
|
||
|
return pc.createOffer({offerToReceiveAudio: false});
|
||
|
})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections.length).to.equal(1);
|
||
|
expect(SDPUtils.getDirection(sections[0])).to.equal('sendonly');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('with an audio track and offering to receive video', () => {
|
||
|
it('the generated SDP should contain a sendrecv audio m-line ' +
|
||
|
'and a recvonly video m-line', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({audio: true})
|
||
|
.then((stream) => {
|
||
|
pc.addTrack(stream.getAudioTracks()[0], stream);
|
||
|
return pc.createOffer({offerToReceiveVideo: true});
|
||
|
})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections.length).to.equal(2);
|
||
|
expect(SDPUtils.getKind(sections[0])).to.equal('audio');
|
||
|
expect(SDPUtils.getDirection(sections[0])).to.equal('sendrecv');
|
||
|
expect(SDPUtils.getKind(sections[1])).to.equal('video');
|
||
|
expect(SDPUtils.getDirection(sections[1])).to.equal('recvonly');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('with a video track', () => {
|
||
|
it('the generated SDP should contain an video m-line', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({video: true})
|
||
|
.then((stream) => {
|
||
|
pc.addTrack(stream.getVideoTracks()[0], stream);
|
||
|
return pc.createOffer();
|
||
|
})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections.length).to.equal(1);
|
||
|
expect(SDPUtils.getKind(sections[0])).to.equal('video');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('with a video track and offerToReceiveAudio', () => {
|
||
|
it('the generated SDP should contain a video and an ' +
|
||
|
'audio m-line', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({video: true})
|
||
|
.then((stream) => {
|
||
|
pc.addTrack(stream.getVideoTracks()[0], stream);
|
||
|
return pc.createOffer({offerToReceiveAudio: true});
|
||
|
})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections.length).to.equal(2);
|
||
|
expect(SDPUtils.getKind(sections[0])).to.equal('video');
|
||
|
expect(SDPUtils.getKind(sections[1])).to.equal('audio');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
|
||
|
describe('with an audio track and a video track', () => {
|
||
|
it('the generated SDP should contain an audio and video ' +
|
||
|
'm-line', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({audio: true, video: true})
|
||
|
.then((stream) => {
|
||
|
stream.getTracks().forEach((track) => {
|
||
|
pc.addTrack(track, stream);
|
||
|
});
|
||
|
return pc.createOffer();
|
||
|
})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections.length).to.equal(2);
|
||
|
expect(SDPUtils.getKind(sections[0])).to.equal('audio');
|
||
|
expect(SDPUtils.getKind(sections[1])).to.equal('video');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('with an audio track and two video tracks', () => {
|
||
|
it('the generated SDP should contain an audio and ' +
|
||
|
'two video m-lines', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({audio: true, video: true})
|
||
|
.then((stream) => {
|
||
|
stream.getTracks().forEach((track) => {
|
||
|
pc.addTrack(track, stream);
|
||
|
});
|
||
|
return navigator.mediaDevices.getUserMedia({video: true});
|
||
|
})
|
||
|
.then((stream) => {
|
||
|
stream.getTracks().forEach((track) => {
|
||
|
pc.addTrack(track, stream);
|
||
|
});
|
||
|
return pc.createOffer();
|
||
|
})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections.length).to.equal(3);
|
||
|
expect(SDPUtils.getKind(sections[0])).to.equal('audio');
|
||
|
expect(SDPUtils.getKind(sections[1])).to.equal('video');
|
||
|
expect(SDPUtils.getKind(sections[2])).to.equal('video');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('with an audio track but no stream', () => {
|
||
|
it('creates an offer with msid stream set to "-"', () => {
|
||
|
return navigator.mediaDevices.getUserMedia({audio: true})
|
||
|
.then((stream) => {
|
||
|
stream.getTracks().forEach((track) => {
|
||
|
pc.addTrack(track);
|
||
|
});
|
||
|
return pc.createOffer();
|
||
|
})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections.length).to.equal(1);
|
||
|
const msid = SDPUtils.parseMsid(sections[0]);
|
||
|
expect(msid.stream).to.equal('-');
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when called subsequently', () => {
|
||
|
let clock;
|
||
|
beforeEach(() => {
|
||
|
clock = sinon.useFakeTimers();
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
clock.restore();
|
||
|
});
|
||
|
|
||
|
it('contains the candidates already emitted', (done) => {
|
||
|
pc.onicegatheringstatechange = () => {
|
||
|
if (pc.iceGatheringState !== 'complete') {
|
||
|
return;
|
||
|
}
|
||
|
pc.createOffer()
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
const candidates = SDPUtils.matchPrefix(sections[0],
|
||
|
'a=candidate:');
|
||
|
const end = SDPUtils.matchPrefix(sections[0],
|
||
|
'a=end-of-candidates');
|
||
|
expect(candidates.length).to.be.above(0);
|
||
|
expect(end.length).to.equal(1);
|
||
|
done();
|
||
|
});
|
||
|
};
|
||
|
pc.createOffer({offerToReceiveAudio: true})
|
||
|
.then((offer) => {
|
||
|
return pc.setLocalDescription(offer);
|
||
|
})
|
||
|
.then(() => {
|
||
|
window.setTimeout(() => {
|
||
|
clock.tick(500);
|
||
|
});
|
||
|
clock.tick(0);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('retains the session id', (done) => {
|
||
|
let sessionId;
|
||
|
pc.createOffer({offerToReceiveAudio: true})
|
||
|
.then((offer) => {
|
||
|
sessionId = SDPUtils.matchPrefix(offer.sdp, 'o=')[0].split(' ')[1];
|
||
|
return pc.createOffer({offerToReceiveAudio: true});
|
||
|
})
|
||
|
.then((offer) => {
|
||
|
let sid = SDPUtils.matchPrefix(offer.sdp, 'o=')[0].split(' ')[1];
|
||
|
expect(sid).to.equal(sessionId);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('increments the session version', (done) => {
|
||
|
let version;
|
||
|
pc.createOffer({offerToReceiveAudio: true})
|
||
|
.then((offer) => {
|
||
|
version = SDPUtils.matchPrefix(offer.sdp, 'o=')[0]
|
||
|
.split(' ')[2] >>> 0;
|
||
|
return pc.createOffer({offerToReceiveAudio: true});
|
||
|
})
|
||
|
.then((offer) => {
|
||
|
let ver = SDPUtils.matchPrefix(offer.sdp, 'o=')[0]
|
||
|
.split(' ')[2] >>> 0;
|
||
|
expect(ver).to.equal(version + 1);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('when called after SRD+createAnswer reversing the roles', () => {
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
|
||
|
|
||
|
it('retains the MID attribute', () => {
|
||
|
return pc.setRemoteDescription({type: 'offer', sdp})
|
||
|
.then(() => pc.createAnswer())
|
||
|
.then((answer) => pc.setLocalDescription(answer))
|
||
|
.then(() => pc.createOffer())
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(SDPUtils.getMid(sections[0])).to.equal('audio1');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('retains the offerer payload types', () => {
|
||
|
return pc.setRemoteDescription({type: 'offer',
|
||
|
sdp: sdp.replace(/111/g, 98)
|
||
|
})
|
||
|
.then(() => pc.createAnswer())
|
||
|
.then((answer) => pc.setLocalDescription(answer))
|
||
|
.then(() => pc.createOffer())
|
||
|
.then((offer) => {
|
||
|
expect(offer.sdp).to.contain('a=rtpmap:98 opus');
|
||
|
expect(offer.sdp).not.to.contain('a=rtpmap:111 opus');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('retains the offerer extmap ids', () => {
|
||
|
const extmapUri = 'http://www.webrtc.org/experiments/' +
|
||
|
'rtp-hdrext/abs-send-time';
|
||
|
const videoSdp = SDP_BOILERPLATE +
|
||
|
'm=video 9 UDP/TLS/RTP/SAVPF 102\r\n' +
|
||
|
'c=IN IP4 0.0.0.0\r\n' +
|
||
|
'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
|
||
|
'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
|
||
|
'a=ice-pwd:' + ICEPWD + '\r\n' +
|
||
|
'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
|
||
|
'a=setup:actpass\r\n' +
|
||
|
'a=mid:video1\r\n' +
|
||
|
'a=sendrecv\r\n' +
|
||
|
'a=rtcp-mux\r\n' +
|
||
|
'a=rtcp-rsize\r\n' +
|
||
|
'a=rtpmap:102 vp8/90000\r\n' +
|
||
|
'a=ssrc:1001 msid:stream1 track1\r\n' +
|
||
|
'a=ssrc:1001 cname:some\r\n' +
|
||
|
'a=extmap:5 ' + extmapUri + '\r\n';
|
||
|
|
||
|
return pc.setRemoteDescription({type: 'offer', sdp: videoSdp})
|
||
|
.then(() => pc.createAnswer())
|
||
|
.then((answer) => pc.setLocalDescription(answer))
|
||
|
.then(() => pc.createOffer())
|
||
|
.then((offer) => {
|
||
|
expect(offer.sdp).to.contain('a=extmap:5 ' + extmapUri + '\r\n');
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('after replaceTrack', () => {
|
||
|
it('retains the original track id', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({audio: true})
|
||
|
.then((stream) => {
|
||
|
pc.addTrack(stream.getAudioTracks()[0], stream);
|
||
|
return pc.createOffer();
|
||
|
})
|
||
|
.then((offer) => pc.setLocalDescription(offer))
|
||
|
.then(() => navigator.mediaDevices.getUserMedia({audio: true}))
|
||
|
.then((stream) => {
|
||
|
const sender = pc.getSenders()[0];
|
||
|
return sender.replaceTrack(stream.getAudioTracks()[0]);
|
||
|
})
|
||
|
.then(() => pc.createOffer())
|
||
|
.then((offer) => {
|
||
|
const newMsid = SDPUtils.parseMsid(offer.sdp);
|
||
|
const existingMsid = SDPUtils.parseMsid(pc.localDescription.sdp);
|
||
|
expect(newMsid.track).to.equal(existingMsid.track);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('createAnswer', () => {
|
||
|
let pc;
|
||
|
beforeEach(() => {
|
||
|
pc = new RTCPeerConnection();
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
pc.close();
|
||
|
});
|
||
|
|
||
|
it('returns a promise', (done) => {
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then(() => {
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('calls the legacy success callback', (done) => {
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer((answer) => {
|
||
|
expect(answer.type).to.equal('answer');
|
||
|
done();
|
||
|
}, () => {});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('calls the legacy success callback and resolves with ' +
|
||
|
'no arguments', (done) => {
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer((answer) => {});
|
||
|
})
|
||
|
.then((shouldBeUndefined) => {
|
||
|
expect(shouldBeUndefined).to.equal(undefined);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('does not change the signaling state', (done) => {
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
expect(pc.signalingState).to.equal('have-remote-offer');
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then(() => {
|
||
|
expect(pc.signalingState).to.equal('have-remote-offer');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('throws an InvalidStateError when called after close', (done) => {
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
pc.close();
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.catch((e) => {
|
||
|
expect(e.name).to.equal('InvalidStateError');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('throws an InvalidStateError when called after close ' +
|
||
|
'(callback)', (done) => {
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
pc.close();
|
||
|
return pc.createAnswer(undefined, (e) => {
|
||
|
expect(e.name).to.equal('InvalidStateError');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('throws an InvalidStateError when called in the wrong ' +
|
||
|
'signalingstate', (done) => {
|
||
|
pc.createAnswer()
|
||
|
.catch((e) => {
|
||
|
expect(e.name).to.equal('InvalidStateError');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('uses payload types of offerer', (done) => {
|
||
|
const sdp = SDP_BOILERPLATE +
|
||
|
'm=audio 9 UDP/TLS/RTP/SAVPF 98\r\n' +
|
||
|
'c=IN IP4 0.0.0.0\r\n' +
|
||
|
'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
|
||
|
'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
|
||
|
'a=ice-pwd:' + ICEPWD + '\r\n' +
|
||
|
'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
|
||
|
'a=setup:actpass\r\n' +
|
||
|
'a=mid:audio1\r\n' +
|
||
|
'a=sendrecv\r\n' +
|
||
|
'a=rtcp-mux\r\n' +
|
||
|
'a=rtcp-rsize\r\n' +
|
||
|
'a=rtpmap:98 opus/48000/2\r\n' +
|
||
|
'a=ssrc:1001 msid:stream1 track1\r\n' +
|
||
|
'a=ssrc:1001 cname:some\r\n';
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
expect(answer.sdp).to.contain('a=rtpmap:98 opus');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('uses the extmap ids of the offerer', (done) => {
|
||
|
const extmapUri = 'http://www.webrtc.org/experiments/' +
|
||
|
'rtp-hdrext/abs-send-time';
|
||
|
const sdp = SDP_BOILERPLATE +
|
||
|
MINIMAL_AUDIO_MLINE +
|
||
|
'm=video 9 UDP/TLS/RTP/SAVPF 102\r\n' +
|
||
|
'c=IN IP4 0.0.0.0\r\n' +
|
||
|
'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
|
||
|
'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
|
||
|
'a=ice-pwd:' + ICEPWD + '\r\n' +
|
||
|
'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
|
||
|
'a=setup:actpass\r\n' +
|
||
|
'a=mid:video1\r\n' +
|
||
|
'a=sendrecv\r\n' +
|
||
|
'a=rtcp-mux\r\n' +
|
||
|
'a=rtcp-rsize\r\n' +
|
||
|
'a=rtpmap:102 vp8/90000\r\n' +
|
||
|
'a=ssrc:1001 msid:stream1 track1\r\n' +
|
||
|
'a=ssrc:1001 cname:some\r\n' +
|
||
|
'a=extmap:5 ' + extmapUri + '\r\n';
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
expect(answer.sdp).to.contain('a=extmap:5 ' + extmapUri + '\r\n');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('returns the intersection of rtcp feedback', (done) => {
|
||
|
const sdp = SDP_BOILERPLATE +
|
||
|
'm=video 9 UDP/TLS/RTP/SAVPF 102\r\n' +
|
||
|
'c=IN IP4 0.0.0.0\r\n' +
|
||
|
'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
|
||
|
'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
|
||
|
'a=ice-pwd:' + ICEPWD + '\r\n' +
|
||
|
'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
|
||
|
'a=setup:actpass\r\n' +
|
||
|
'a=mid:video1\r\n' +
|
||
|
'a=sendrecv\r\n' +
|
||
|
'a=rtcp-mux\r\n' +
|
||
|
'a=rtcp-rsize\r\n' +
|
||
|
'a=rtpmap:102 vp8/90000\r\n' +
|
||
|
'a=rtcp-fb:102 nack\r\n' +
|
||
|
'a=rtcp-fb:102 nack pli\r\n' +
|
||
|
'a=rtcp-fb:102 goog-remb\r\n' +
|
||
|
'a=ssrc:1001 msid:stream1 track1\r\n' +
|
||
|
'a=ssrc:1001 cname:some\r\n';
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
expect(answer.sdp).to.contain('a=rtcp-fb:102 nack\r\n');
|
||
|
expect(answer.sdp).to.contain('a=rtcp-fb:102 nack pli\r\n');
|
||
|
expect(answer.sdp).not.to.contain('a=rtcp-fb:102 goog-remb\r\n');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('rejects a m-line when there are no compatible codecs', (done) => {
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
|
||
|
pc.setRemoteDescription({type: 'offer',
|
||
|
sdp: sdp.replace('opus', 'nosuchcodec')
|
||
|
})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
const sections = SDPUtils.getMediaSections(answer.sdp);
|
||
|
const rejected = SDPUtils.isRejected(sections[0]);
|
||
|
expect(rejected).to.equal(true);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('rejects a legacy datachannel offer', () => {
|
||
|
const sdp = SDP_BOILERPLATE +
|
||
|
'm=application 9 DTLS/SCTP 5000\r\n' +
|
||
|
'c=IN IP4 0.0.0.0\r\n' +
|
||
|
'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
|
||
|
'a=ice-pwd:' + ICEPWD + '\r\n' +
|
||
|
'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
|
||
|
'a=setup:actpass\r\n' +
|
||
|
'a=mid:data\r\n' +
|
||
|
'a=sctpmap:5000 webrtc-datachannel 1024\r\n';
|
||
|
it('in setRemoteDescription', (done) => {
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
const sections = SDPUtils.getMediaSections(answer.sdp);
|
||
|
const rejected = SDPUtils.isRejected(sections[0]);
|
||
|
expect(rejected).to.equal(true);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('ignores candidates', () => {
|
||
|
return pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
return pc.addIceCandidate({sdpMid: 'data', candidate:
|
||
|
'candidate:702786350 1 udp 41819902 8.8.8.8 60769 typ host'});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('ignores end-of-candidates', () => {
|
||
|
return pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => pc.addIceCandidate());
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('rejects a new-style datachannel offer', () => {
|
||
|
it('in setRemoteDescription', () => {
|
||
|
const sdp = SDP_BOILERPLATE +
|
||
|
'm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n' +
|
||
|
'c=IN IP4 0.0.0.0\r\n' +
|
||
|
'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
|
||
|
'a=ice-pwd:' + ICEPWD + '\r\n' +
|
||
|
'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
|
||
|
'a=setup:actpass\r\n' +
|
||
|
'a=mid:data\r\n' +
|
||
|
'a=sctp-port:5000\r\n' +
|
||
|
'a=max-message-size:1073741823\r\n';
|
||
|
return pc.setRemoteDescription({type: 'offer', sdp})
|
||
|
.then(() => pc.createAnswer())
|
||
|
.then((answer) => {
|
||
|
const sections = SDPUtils.getMediaSections(answer.sdp);
|
||
|
const rejected = SDPUtils.isRejected(sections[0]);
|
||
|
expect(rejected).to.equal(true);
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
// test https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-15#section-5.3.4
|
||
|
describe('direction attribute', () => {
|
||
|
const sdp = SDP_BOILERPLATE +
|
||
|
'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' +
|
||
|
'c=IN IP4 0.0.0.0\r\n' +
|
||
|
'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
|
||
|
'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
|
||
|
'a=ice-pwd:' + ICEPWD + '\r\n' +
|
||
|
'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
|
||
|
'a=setup:actpass\r\n' +
|
||
|
'a=mid:audio1\r\n' +
|
||
|
'a=sendrecv\r\n' +
|
||
|
'a=rtcp-mux\r\n' +
|
||
|
'a=rtcp-rsize\r\n' +
|
||
|
'a=rtpmap:111 opus/48000/2\r\n' +
|
||
|
'a=ssrc:1001 msid:stream1 track1\r\n' +
|
||
|
'a=ssrc:1001 cname:some\r\n';
|
||
|
|
||
|
it('responds with a inactive answer to inactive', (done) => {
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp.replace('sendrecv',
|
||
|
'recvonly')})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
const sections = SDPUtils.getMediaSections(answer.sdp);
|
||
|
expect(sections.length).to.equal(1);
|
||
|
expect(SDPUtils.getDirection(sections[0])).to.equal('inactive');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('with a local track', () => {
|
||
|
it('responds with a sendrecv answer to sendrecv', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({audio: true})
|
||
|
.then((stream) => {
|
||
|
pc.addStream(stream);
|
||
|
return pc.setRemoteDescription({type: 'offer', sdp: sdp});
|
||
|
})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
const sections = SDPUtils.getMediaSections(answer.sdp);
|
||
|
expect(SDPUtils.getDirection(sections[0])).to.equal('sendrecv');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('responds with a sendonly answer to recvonly', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({audio: true})
|
||
|
.then((stream) => {
|
||
|
pc.addStream(stream);
|
||
|
return pc.setRemoteDescription({type: 'offer',
|
||
|
sdp: sdp.replace('sendrecv', 'recvonly')
|
||
|
});
|
||
|
})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
const sections = SDPUtils.getMediaSections(answer.sdp);
|
||
|
expect(sections.length).to.equal(1);
|
||
|
expect(SDPUtils.getDirection(sections[0])).to.equal('sendonly');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('with a local track added after setRemoteDescription', () => {
|
||
|
it('responds with a sendrecv answer to sendrecv', (done) => {
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
return navigator.mediaDevices.getUserMedia({audio: true});
|
||
|
})
|
||
|
.then((stream) => {
|
||
|
pc.addStream(stream);
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
const sections = SDPUtils.getMediaSections(answer.sdp);
|
||
|
expect(SDPUtils.getDirection(sections[0])).to.equal('sendrecv');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('responds with a sendonly answer to recvonly', (done) => {
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp.replace('sendrecv',
|
||
|
'recvonly')})
|
||
|
.then(() => {
|
||
|
return navigator.mediaDevices.getUserMedia({audio: true});
|
||
|
})
|
||
|
.then((stream) => {
|
||
|
pc.addStream(stream);
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
const sections = SDPUtils.getMediaSections(answer.sdp);
|
||
|
expect(sections.length).to.equal(1);
|
||
|
expect(SDPUtils.getDirection(sections[0])).to.equal('sendonly');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('with no local track', () => {
|
||
|
it('responds with a recvonly answer to sendrecv', (done) => {
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
const sections = SDPUtils.getMediaSections(answer.sdp);
|
||
|
expect(SDPUtils.getDirection(sections[0])).to.equal('recvonly');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('responds with a inactive answer to recvonly', (done) => {
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp.replace('sendrecv',
|
||
|
'recvonly')})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
const sections = SDPUtils.getMediaSections(answer.sdp);
|
||
|
expect(SDPUtils.getDirection(sections[0])).to.equal('inactive');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('after a video offer with RTX', () => {
|
||
|
const sdp = SDP_BOILERPLATE +
|
||
|
'm=video 9 UDP/TLS/RTP/SAVPF 102 103\r\n' +
|
||
|
'c=IN IP4 0.0.0.0\r\n' +
|
||
|
'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
|
||
|
'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
|
||
|
'a=ice-pwd:' + ICEPWD + '\r\n' +
|
||
|
'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
|
||
|
'a=setup:actpass\r\n' +
|
||
|
'a=mid:video1\r\n' +
|
||
|
'a=sendrecv\r\n' +
|
||
|
'a=rtcp-mux\r\n' +
|
||
|
'a=rtcp-rsize\r\n' +
|
||
|
'a=rtpmap:102 vp8/90000\r\n' +
|
||
|
'a=rtpmap:103 rtx/90000\r\n' +
|
||
|
'a=fmtp:103 apt=102\r\n';
|
||
|
const remoteRTX = 'a=ssrc-group:FID 1001 1002\r\n' +
|
||
|
'a=ssrc:1001 msid:stream1 track1\r\n' +
|
||
|
'a=ssrc:1001 cname:some\r\n' +
|
||
|
'a=ssrc:1002 msid:stream1 track1\r\n' +
|
||
|
'a=ssrc:1002 cname:some\r\n';
|
||
|
describe('with no local track', () => {
|
||
|
it('creates an answer with RTX but no FID group', (done) => {
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp + remoteRTX})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
expect(answer.sdp).to.contain('a=rtpmap:102 vp8');
|
||
|
expect(answer.sdp).to.contain('a=rtpmap:103 rtx');
|
||
|
expect(answer.sdp).to.contain('a=fmtp:103 apt=102');
|
||
|
expect(answer.sdp).not.to.contain('a=ssrc-group:FID ');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('with a local track', () => {
|
||
|
it('creates an answer with RTX', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({video: true})
|
||
|
.then((stream) => {
|
||
|
pc.addStream(stream);
|
||
|
return pc.setRemoteDescription({type: 'offer',
|
||
|
sdp: sdp + remoteRTX});
|
||
|
})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
expect(answer.sdp).to.contain('a=rtpmap:102 vp8');
|
||
|
expect(answer.sdp).to.contain('a=rtpmap:103 rtx');
|
||
|
expect(answer.sdp).to.contain('a=fmtp:103 apt=102');
|
||
|
expect(answer.sdp).to.contain('a=ssrc-group:FID ');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('with no remote track', () => {
|
||
|
it('creates an answer with RTX', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({video: true})
|
||
|
.then((stream) => {
|
||
|
pc.addStream(stream);
|
||
|
return pc.setRemoteDescription({type: 'offer',
|
||
|
sdp: sdp.replace('sendrecv', 'recvonly')});
|
||
|
})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
expect(answer.sdp).to.contain('a=rtpmap:102 vp8');
|
||
|
expect(answer.sdp).to.contain('a=rtpmap:103 rtx');
|
||
|
expect(answer.sdp).to.contain('a=fmtp:103 apt=102');
|
||
|
expect(answer.sdp).to.contain('a=ssrc-group:FID ');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('but mismatching video codec', () => {
|
||
|
it('creates an answer without RTX', (done) => {
|
||
|
const modifiedSDP = SDP_BOILERPLATE +
|
||
|
'm=video 9 UDP/TLS/RTP/SAVPF 101 102 103\r\n' +
|
||
|
'c=IN IP4 0.0.0.0\r\n' +
|
||
|
'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
|
||
|
'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
|
||
|
'a=ice-pwd:' + ICEPWD + '\r\n' +
|
||
|
'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
|
||
|
'a=setup:actpass\r\n' +
|
||
|
'a=mid:video1\r\n' +
|
||
|
'a=sendrecv\r\n' +
|
||
|
'a=rtcp-mux\r\n' +
|
||
|
'a=rtcp-rsize\r\n' +
|
||
|
'a=rtpmap:101 vp8/90000\r\n' +
|
||
|
'a=rtpmap:102 no-such-codec/90000\r\n' +
|
||
|
'a=rtpmap:103 rtx/90000\r\n' +
|
||
|
'a=fmtp:103 apt=102\r\n';
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: modifiedSDP})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
expect(answer.sdp).to.contain('a=rtpmap:101 vp8');
|
||
|
expect(answer.sdp).not.to.contain('a=rtpmap:102 no-such-codec');
|
||
|
expect(answer.sdp).not.to.contain('a=rtpmap:103 rtx');
|
||
|
expect(answer.sdp).not.to.contain('a=fmtp:103 apt=102');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('after a video offer without RTX', () => {
|
||
|
const sdp = SDP_BOILERPLATE +
|
||
|
'm=video 9 UDP/TLS/RTP/SAVPF 102\r\n' +
|
||
|
'c=IN IP4 0.0.0.0\r\n' +
|
||
|
'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
|
||
|
'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
|
||
|
'a=ice-pwd:' + ICEPWD + '\r\n' +
|
||
|
'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
|
||
|
'a=setup:actpass\r\n' +
|
||
|
'a=mid:video1\r\n' +
|
||
|
'a=sendrecv\r\n' +
|
||
|
'a=rtcp-mux\r\n' +
|
||
|
'a=rtcp-rsize\r\n' +
|
||
|
'a=rtpmap:102 vp8/90000\r\n' +
|
||
|
'a=ssrc:1001 msid:stream1 track1\r\n' +
|
||
|
'a=ssrc:1001 cname:some\r\n';
|
||
|
it('there is no ssrc-group in the answer', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({video: true})
|
||
|
.then((stream) => {
|
||
|
pc.addStream(stream);
|
||
|
return pc.setRemoteDescription({type: 'offer', sdp: sdp});
|
||
|
})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
expect(answer.sdp).not.to.contain('a=ssrc-group:FID ');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('after an offer containing a rejected mline', () => {
|
||
|
it('rejects the m-line in the answer', () => {
|
||
|
const sdp = SDP_BOILERPLATE +
|
||
|
MINIMAL_AUDIO_MLINE.replace('m=audio 9', 'm=audio 0');
|
||
|
return pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
const sections = SDPUtils.getMediaSections(answer.sdp);
|
||
|
expect(sections.length).to.equal(1);
|
||
|
expect(SDPUtils.getKind(sections[0])).to.equal('audio');
|
||
|
expect(SDPUtils.isRejected(sections[0])).to.equal(true);
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('rtcp-rsize is', () => {
|
||
|
const sdp = SDP_BOILERPLATE +
|
||
|
'm=video 9 UDP/TLS/RTP/SAVPF 102\r\n' +
|
||
|
'c=IN IP4 0.0.0.0\r\n' +
|
||
|
'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
|
||
|
'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
|
||
|
'a=ice-pwd:' + ICEPWD + '\r\n' +
|
||
|
'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
|
||
|
'a=setup:actpass\r\n' +
|
||
|
'a=mid:video1\r\n' +
|
||
|
'a=sendrecv\r\n' +
|
||
|
'a=rtcp-mux\r\n' +
|
||
|
'a=rtcp-rsize\r\n' +
|
||
|
'a=rtpmap:102 vp8/90000\r\n' +
|
||
|
'a=ssrc:1001 msid:stream1 track1\r\n' +
|
||
|
'a=ssrc:1001 cname:some\r\n';
|
||
|
|
||
|
it('set if the offer contained rtcp-rsize', (done) => {
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
expect(answer.sdp).to.contain('a=rtcp-rsize\r\n');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('not set if the offer did not contain rtcp-rsize', (done) => {
|
||
|
pc.setRemoteDescription({type: 'offer',
|
||
|
sdp: sdp.replace('a=rtcp-rsize\r\n', '')})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
expect(answer.sdp).not.to.contain('a=rtcp-rsize\r\n');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('with the remote offering BUNDLE', () => {
|
||
|
let clock;
|
||
|
beforeEach(() => {
|
||
|
clock = sinon.useFakeTimers();
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
clock.restore();
|
||
|
});
|
||
|
|
||
|
const sdp = SDP_BOILERPLATE +
|
||
|
'a=group:BUNDLE audio1 video1\r\n' +
|
||
|
'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' +
|
||
|
'c=IN IP4 0.0.0.0\r\n' +
|
||
|
'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
|
||
|
'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
|
||
|
'a=ice-pwd:' + ICEPWD + '\r\n' +
|
||
|
'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
|
||
|
'a=setup:actpass\r\n' +
|
||
|
'a=mid:audio1\r\n' +
|
||
|
'a=sendonly\r\n' +
|
||
|
'a=rtcp-mux\r\n' +
|
||
|
'a=rtcp-rsize\r\n' +
|
||
|
'a=rtpmap:111 opus/48000/2\r\n' +
|
||
|
'a=ssrc:1001 cname:some\r\n' +
|
||
|
'm=video 9 UDP/TLS/RTP/SAVPF 102\r\n' +
|
||
|
'c=IN IP4 0.0.0.0\r\n' +
|
||
|
'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
|
||
|
'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
|
||
|
'a=ice-pwd:' + ICEPWD + '\r\n' +
|
||
|
'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
|
||
|
'a=setup:actpass\r\n' +
|
||
|
'a=mid:video1\r\n' +
|
||
|
'a=sendrecv\r\n' +
|
||
|
'a=rtcp-mux\r\n' +
|
||
|
'a=rtcp-rsize\r\n' +
|
||
|
'a=rtpmap:102 vp8/90000\r\n' +
|
||
|
'a=ssrc:1002 cname:some\r\n';
|
||
|
it('does not send candidates with sdpMLineIndex=1', (done) => {
|
||
|
pc.onicecandidate = sinon.stub();
|
||
|
pc.onicegatheringstatechange = () => {
|
||
|
if (pc.iceGatheringState === 'complete') {
|
||
|
expect(pc.onicecandidate).to.have.been.calledWith(sinon.match({
|
||
|
candidate: sinon.match({sdpMLineIndex: sinon.match(0)})
|
||
|
}));
|
||
|
expect(pc.onicecandidate).not.to.have.been.calledWith(sinon.match({
|
||
|
candidate: sinon.match({sdpMLineIndex: sinon.match(1)})
|
||
|
}));
|
||
|
done();
|
||
|
}
|
||
|
};
|
||
|
pc.setRemoteDescription({type: 'offer', sdp})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
return pc.setLocalDescription(answer);
|
||
|
})
|
||
|
.then(() => {
|
||
|
window.setTimeout(() => {
|
||
|
clock.tick(500);
|
||
|
});
|
||
|
clock.tick(0);
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('session version handling', () => {
|
||
|
it('starts at version 0', (done) => {
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
let ver = SDPUtils.matchPrefix(answer.sdp, 'o=')[0]
|
||
|
.split(' ')[2] >>> 0;
|
||
|
expect(ver).to.equal(0);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('subsequent calls increase the session version', (done) => {
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
let ver = SDPUtils.matchPrefix(answer.sdp, 'o=')[0]
|
||
|
.split(' ')[2] >>> 0;
|
||
|
expect(ver).to.equal(1);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('with an audio-only offer adding an ' +
|
||
|
'audio/video stream', () => {
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
|
||
|
it('does not try to add a video m-line', (done) => {
|
||
|
// https://github.com/webrtc/adapter/issues/638
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
return navigator.mediaDevices.getUserMedia({audio: true,
|
||
|
video: true});
|
||
|
})
|
||
|
.then((stream) => {
|
||
|
pc.addStream(stream);
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
const sections = SDPUtils.getMediaSections(answer.sdp);
|
||
|
expect(sections.length).to.equal(1);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('addIceCandidate', () => {
|
||
|
const sdp = SDP_BOILERPLATE +
|
||
|
'a=group:BUNDLE audio1 video1\r\n' +
|
||
|
'm=audio 9 UDP/TLS/RTP/SAVPF 98\r\n' +
|
||
|
'c=IN IP4 0.0.0.0\r\n' +
|
||
|
'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
|
||
|
'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
|
||
|
'a=ice-pwd:' + ICEPWD + '\r\n' +
|
||
|
'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
|
||
|
'a=setup:actpass\r\n' +
|
||
|
'a=mid:audio1\r\n' +
|
||
|
'a=sendrecv\r\n' +
|
||
|
'a=rtcp-mux\r\n' +
|
||
|
'a=rtcp-rsize\r\n' +
|
||
|
'a=rtpmap:98 opus/48000/2\r\n' +
|
||
|
'a=ssrc:1001 msid:stream1 track1\r\n' +
|
||
|
'a=ssrc:1001 cname:some\r\n' +
|
||
|
'm=video 9 UDP/TLS/RTP/SAVPF 102 103\r\n' +
|
||
|
'c=IN IP4 0.0.0.0\r\n' +
|
||
|
'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
|
||
|
'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
|
||
|
'a=ice-pwd:' + ICEPWD + '\r\n' +
|
||
|
'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
|
||
|
'a=setup:actpass\r\n' +
|
||
|
'a=mid:video1\r\n' +
|
||
|
'a=sendrecv\r\n' +
|
||
|
'a=rtcp-mux\r\n' +
|
||
|
'a=rtcp-rsize\r\n' +
|
||
|
'a=rtpmap:102 vp8/90000\r\n' +
|
||
|
'a=rtpmap:103 rtx/90000\r\n' +
|
||
|
'a=fmtp:103 apt=102\r\n' +
|
||
|
'a=ssrc-group:FID 1001 1002\r\n' +
|
||
|
'a=ssrc:1001 msid:stream1 track1\r\n' +
|
||
|
'a=ssrc:1001 cname:some\r\n' +
|
||
|
'a=ssrc:1002 msid:stream1 track1\r\n' +
|
||
|
'a=ssrc:1002 cname:some\r\n';
|
||
|
const candidateString = 'candidate:702786350 1 udp 41819902 8.8.8.8 ' +
|
||
|
'60769 typ host';
|
||
|
const sdpMid = 'audio1';
|
||
|
|
||
|
let pc;
|
||
|
beforeEach((done) => {
|
||
|
pc = new RTCPeerConnection();
|
||
|
pc.setRemoteDescription({type: 'offer', sdp})
|
||
|
.then(done);
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
pc.close();
|
||
|
});
|
||
|
|
||
|
it('returns a promise', (done) => {
|
||
|
pc.addIceCandidate({sdpMid, candidate: candidateString})
|
||
|
.then(done);
|
||
|
});
|
||
|
|
||
|
it('calls the legacy success callback', (done) => {
|
||
|
pc.addIceCandidate({sdpMid, candidate: candidateString}, done, () => {});
|
||
|
});
|
||
|
|
||
|
it('throws a TypeError when called without sdpMid or ' +
|
||
|
'sdpMLineIndex', (done) => {
|
||
|
pc.addIceCandidate({})
|
||
|
.catch((e) => {
|
||
|
expect(e.name).to.equal('TypeError');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('rejects with an OperationError when called with an', () => {
|
||
|
it('invalid sdpMid', (done) => {
|
||
|
pc.addIceCandidate({sdpMid: 'invalid', candidate: candidateString})
|
||
|
.catch((e) => {
|
||
|
expect(e.name).to.equal('OperationError');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('invalid sdpMLineIndex', (done) => {
|
||
|
pc.addIceCandidate({sdpMLineIndex: 99, candidate: candidateString})
|
||
|
.catch((e) => {
|
||
|
expect(e.name).to.equal('OperationError');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('calls the legacy error callback when called with an ' +
|
||
|
'invalid sdpMLineIndex', (done) => {
|
||
|
pc.addIceCandidate({sdpMLineIndex: 99, candidate: candidateString},
|
||
|
() => {},
|
||
|
(e) => {
|
||
|
expect(e.name).to.equal('OperationError');
|
||
|
done();
|
||
|
}
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it('rejects with an InvalidStateError when called before ' +
|
||
|
'setRemoteDescription', (done) => {
|
||
|
pc = new RTCPeerConnection(); // recreate pc.
|
||
|
pc.addIceCandidate({sdpMid, candidate: candidateString})
|
||
|
.catch((e) => {
|
||
|
expect(e.name).to.equal('InvalidStateError');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('adds the candidate to the remote description', (done) => {
|
||
|
pc.addIceCandidate({sdpMid, candidate: candidateString})
|
||
|
.then(() => {
|
||
|
const sections = SDPUtils.getMediaSections(pc.remoteDescription.sdp);
|
||
|
expect(SDPUtils.matchPrefix(sections[0],
|
||
|
'a=candidate:')).to.have.length(1);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('adds the candidate to the remote description ' +
|
||
|
'with legacy a=candidate syntax', (done) => {
|
||
|
pc.addIceCandidate({sdpMid, candidate: 'a=' + candidateString})
|
||
|
.then(() => {
|
||
|
expect(SDPUtils.matchPrefix(pc.remoteDescription.sdp,
|
||
|
'a=candidate:')).to.have.length(1);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('adds end-of-candidates when receiving the null candidate', (done) => {
|
||
|
// add at least one valid candidate.
|
||
|
pc.addIceCandidate({sdpMid, candidate: candidateString});
|
||
|
pc.addIceCandidate()
|
||
|
.then(() => {
|
||
|
expect(SDPUtils.matchPrefix(pc.remoteDescription.sdp,
|
||
|
'a=end-of-candidates')).to.have.length(1);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('adds end-of-candidates when receiving the \'\' candidate', (done) => {
|
||
|
// add at least one valid candidate.
|
||
|
pc.addIceCandidate({sdpMid, candidate: candidateString});
|
||
|
pc.addIceCandidate({sdpMid, candidate: ''})
|
||
|
.then(() => {
|
||
|
expect(SDPUtils.matchPrefix(pc.remoteDescription.sdp,
|
||
|
'a=end-of-candidates')).to.have.length(1);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('ignores candidates with', () => {
|
||
|
it('component=2 and does not add them to the sdp', (done) => {
|
||
|
const iceTransport = pc.getReceivers()[0].transport.transport;
|
||
|
sinon.spy(iceTransport, 'addRemoteCandidate');
|
||
|
pc.addIceCandidate({sdpMid, candidate:
|
||
|
candidateString.replace('1 udp', '2 udp')})
|
||
|
.then(() => {
|
||
|
expect(iceTransport.addRemoteCandidate).not.to.have.been.calledWith();
|
||
|
expect(SDPUtils.matchPrefix(pc.remoteDescription.sdp,
|
||
|
'a=candidate:')).to.have.length(0);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('non-master mid but does add them to the sdp', (done) => {
|
||
|
const iceTransport = pc.getReceivers()[0].transport.transport;
|
||
|
sinon.spy(iceTransport, 'addRemoteCandidate');
|
||
|
pc.addIceCandidate({sdpMid: 'video1', candidate: candidateString})
|
||
|
.then(() => {
|
||
|
expect(iceTransport.addRemoteCandidate).not.to.have.been.calledWith();
|
||
|
expect(SDPUtils.matchPrefix(pc.remoteDescription.sdp,
|
||
|
'a=candidate:')).to.have.length(1);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('port 0 and does not add them to the sdp', (done) => {
|
||
|
const iceTransport = pc.getReceivers()[0].transport.transport;
|
||
|
sinon.spy(iceTransport, 'addRemoteCandidate');
|
||
|
pc.addIceCandidate({sdpMid, candidate:
|
||
|
candidateString.replace('60769', '0').replace('udp', 'tcp')})
|
||
|
.then(() => {
|
||
|
expect(iceTransport.addRemoteCandidate).not.to.have.been.calledWith();
|
||
|
expect(SDPUtils.matchPrefix(pc.remoteDescription.sdp,
|
||
|
'a=candidate:')).to.have.length(0);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('port 9 and does not add them to the sdp', (done) => {
|
||
|
const iceTransport = pc.getReceivers()[0].transport.transport;
|
||
|
sinon.spy(iceTransport, 'addRemoteCandidate');
|
||
|
pc.addIceCandidate({sdpMid, candidate:
|
||
|
candidateString.replace('60769', '9').replace('udp', 'tcp')})
|
||
|
.then(() => {
|
||
|
expect(iceTransport.addRemoteCandidate).not.to.have.been.calledWith();
|
||
|
expect(SDPUtils.matchPrefix(pc.remoteDescription.sdp,
|
||
|
'a=candidate:')).to.have.length(0);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('negotiationneeded', () => {
|
||
|
let pc;
|
||
|
beforeEach(() => {
|
||
|
pc = new RTCPeerConnection();
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
pc.close();
|
||
|
});
|
||
|
|
||
|
it('fires as an event', (done) => {
|
||
|
const stub = sinon.stub();
|
||
|
pc.addEventListener('negotiationneeded', stub);
|
||
|
|
||
|
navigator.mediaDevices.getUserMedia({audio: true})
|
||
|
.then((stream) => {
|
||
|
pc.addTrack(stream.getAudioTracks()[0], stream);
|
||
|
})
|
||
|
.then(() => {
|
||
|
setTimeout(() => {
|
||
|
expect(stub).to.have.been.calledOnce();
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('triggers after', () => {
|
||
|
it('addTrack', (done) => {
|
||
|
pc.onnegotiationneeded = sinon.stub();
|
||
|
|
||
|
navigator.mediaDevices.getUserMedia({audio: true})
|
||
|
.then((stream) => {
|
||
|
pc.addTrack(stream.getAudioTracks()[0], stream);
|
||
|
})
|
||
|
.then(() => {
|
||
|
setTimeout(() => {
|
||
|
expect(pc.onnegotiationneeded).to.have.been.calledOnce();
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('addStream', (done) => {
|
||
|
pc.onnegotiationneeded = sinon.stub();
|
||
|
|
||
|
navigator.mediaDevices.getUserMedia({audio: true, video: true})
|
||
|
.then((stream) => {
|
||
|
pc.addStream(stream);
|
||
|
})
|
||
|
.then(() => {
|
||
|
setTimeout(() => {
|
||
|
expect(pc.onnegotiationneeded).to.have.been.calledOnce();
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('does not trigger when already needing negotiation', (done) => {
|
||
|
pc.onnegotiationneeded = sinon.stub();
|
||
|
|
||
|
navigator.mediaDevices.getUserMedia({audio: true, video: true})
|
||
|
.then((stream) => {
|
||
|
stream.getTracks().forEach((track) => {
|
||
|
pc.addTrack(track, stream);
|
||
|
});
|
||
|
})
|
||
|
.then(() => {
|
||
|
setTimeout(() => {
|
||
|
expect(pc.onnegotiationneeded).to.have.been.calledOnce();
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('full cycle', () => {
|
||
|
let pc1;
|
||
|
let pc2;
|
||
|
beforeEach(() => {
|
||
|
pc1 = new RTCPeerConnection();
|
||
|
pc2 = new RTCPeerConnection();
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
pc1.close();
|
||
|
pc2.close();
|
||
|
});
|
||
|
|
||
|
it('completes a full createOffer-SLD-SRD-createAnswer-SLD-SRD ' +
|
||
|
'cycle', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({audio: true, video: true})
|
||
|
.then((stream) => {
|
||
|
pc1.addStream(stream);
|
||
|
pc2.addStream(stream);
|
||
|
return pc1.createOffer();
|
||
|
})
|
||
|
.then((offer) => pc1.setLocalDescription(offer))
|
||
|
.then(() => pc2.setRemoteDescription(pc1.localDescription))
|
||
|
.then(() => pc2.createAnswer())
|
||
|
.then((answer) => pc2.setLocalDescription(answer))
|
||
|
.then(() => pc1.setRemoteDescription(pc2.localDescription))
|
||
|
.then(() => {
|
||
|
expect(pc1.signalingState).to.equal('stable');
|
||
|
expect(pc2.signalingState).to.equal('stable');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('remote reoffer with role change', () => {
|
||
|
let pc1;
|
||
|
let pc2;
|
||
|
beforeEach(() => {
|
||
|
pc1 = new RTCPeerConnection();
|
||
|
pc2 = new RTCPeerConnection();
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
pc1.close();
|
||
|
pc2.close();
|
||
|
});
|
||
|
|
||
|
it('retains SSRCs', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({audio: true, video: true})
|
||
|
.then((stream) => {
|
||
|
pc1.addStream(stream);
|
||
|
return pc1.createOffer();
|
||
|
})
|
||
|
.then((offer) => pc1.setLocalDescription(offer))
|
||
|
.then(() => pc2.setRemoteDescription(pc1.localDescription))
|
||
|
.then(() => pc2.createAnswer())
|
||
|
.then((answer) => pc2.setLocalDescription(answer))
|
||
|
.then(() => pc1.setRemoteDescription(pc2.localDescription))
|
||
|
.then(() => {
|
||
|
return navigator.mediaDevices.getUserMedia({audio: true, video: true});
|
||
|
})
|
||
|
.then((stream) => {
|
||
|
pc2.addStream(stream);
|
||
|
return pc2.createOffer();
|
||
|
})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections.length).to.equal(2);
|
||
|
const audioEncodingParameters = SDPUtils.parseRtpEncodingParameters(
|
||
|
sections[0]);
|
||
|
const videoEncodingParameters = SDPUtils.parseRtpEncodingParameters(
|
||
|
sections[1]);
|
||
|
expect(audioEncodingParameters[0].ssrc).to.equal(2002);
|
||
|
expect(videoEncodingParameters[0].ssrc).to.equal(4004);
|
||
|
expect(videoEncodingParameters[0].rtx.ssrc).to.equal(4005);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('sets the right DTLS role in the answer', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({audio: true, video: true})
|
||
|
.then((stream) => {
|
||
|
pc1.addStream(stream);
|
||
|
return pc1.createOffer();
|
||
|
})
|
||
|
.then((offer) => pc1.setLocalDescription(offer))
|
||
|
.then(() => pc2.setRemoteDescription(pc1.localDescription))
|
||
|
.then(() => pc2.createAnswer())
|
||
|
.then((answer) => pc2.setLocalDescription(answer))
|
||
|
.then(() => pc1.setRemoteDescription(pc2.localDescription))
|
||
|
.then(() => {
|
||
|
return navigator.mediaDevices.getUserMedia({audio: true, video: true});
|
||
|
})
|
||
|
.then((stream) => {
|
||
|
pc2.addStream(stream);
|
||
|
return pc2.createOffer();
|
||
|
})
|
||
|
.then((offer) => pc2.setLocalDescription(offer))
|
||
|
.then(() => pc1.setRemoteDescription(pc2.localDescription))
|
||
|
.then(() => pc1.createAnswer())
|
||
|
.then((answer) => {
|
||
|
const sections = SDPUtils.getMediaSections(answer.sdp);
|
||
|
expect(sections.length).to.equal(2);
|
||
|
const setupLine = SDPUtils.matchPrefix(sections[0], 'a=setup:');
|
||
|
expect(setupLine[0]).to.equal('a=setup:passive');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('bundlePolicy', () => {
|
||
|
it('creates an offer with a=group:BUNDLE by default', (done) => {
|
||
|
const pc = new RTCPeerConnection();
|
||
|
|
||
|
pc.createOffer({offerToReceiveAudio: true})
|
||
|
.then((offer) => {
|
||
|
expect(offer.sdp).to.contain('a=group:BUNDLE');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('max-compat creates an offer without a=group:BUNDLE', (done) => {
|
||
|
const pc = new RTCPeerConnection({bundlePolicy: 'max-compat'});
|
||
|
|
||
|
pc.createOffer({offerToReceiveAudio: true})
|
||
|
.then((offer) => {
|
||
|
expect(offer.sdp).not.to.contain('a=group:BUNDLE');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('emits candidates with sdpMLineIndex', () => {
|
||
|
let clock;
|
||
|
beforeEach(() => {
|
||
|
clock = sinon.useFakeTimers();
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
clock.restore();
|
||
|
});
|
||
|
|
||
|
it('1 and 2 when using max-compat', (done) => {
|
||
|
const pc = new RTCPeerConnection({bundlePolicy: 'max-compat'});
|
||
|
|
||
|
pc.onicecandidate = sinon.stub();
|
||
|
pc.onicegatheringstatechange = () => {
|
||
|
if (pc.iceGatheringState === 'complete') {
|
||
|
expect(pc.onicecandidate).to.have.been.calledWith(sinon.match({
|
||
|
candidate: sinon.match({sdpMLineIndex: sinon.match(0)})
|
||
|
}));
|
||
|
expect(pc.onicecandidate).to.have.been.calledWith(sinon.match({
|
||
|
candidate: sinon.match({sdpMLineIndex: sinon.match(1)})
|
||
|
}));
|
||
|
done();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
pc.createOffer({offerToReceiveAudio: true, offerToReceiveVideo: true})
|
||
|
.then((offer) => {
|
||
|
return pc.setLocalDescription(offer);
|
||
|
})
|
||
|
.then(() => {
|
||
|
window.setTimeout(() => {
|
||
|
clock.tick(500);
|
||
|
});
|
||
|
clock.tick(0);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('1 when using max-bundle', (done) => {
|
||
|
const pc = new RTCPeerConnection({bundlePolicy: 'max-bundle'});
|
||
|
|
||
|
pc.onicecandidate = sinon.stub();
|
||
|
pc.onicegatheringstatechange = () => {
|
||
|
if (pc.iceGatheringState === 'complete') {
|
||
|
expect(pc.onicecandidate).to.have.been.calledWith(sinon.match({
|
||
|
candidate: sinon.match({sdpMLineIndex: sinon.match(0)})
|
||
|
}));
|
||
|
expect(pc.onicecandidate).not.to.have.been.calledWith(sinon.match({
|
||
|
candidate: sinon.match({sdpMLineIndex: sinon.match(1)})
|
||
|
}));
|
||
|
done();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
pc.createOffer({offerToReceiveAudio: true, offerToReceiveVideo: true})
|
||
|
.then((offer) => {
|
||
|
return pc.setLocalDescription(offer);
|
||
|
})
|
||
|
.then(() => {
|
||
|
window.setTimeout(() => {
|
||
|
clock.tick(500);
|
||
|
});
|
||
|
clock.tick(0);
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('getSenders', () => {
|
||
|
let pc;
|
||
|
beforeEach(() => {
|
||
|
pc = new RTCPeerConnection();
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
pc.close();
|
||
|
});
|
||
|
|
||
|
it('returns an empty array initially', () => {
|
||
|
expect(pc.getSenders().length).to.equal(0);
|
||
|
});
|
||
|
|
||
|
it('returns a single element after addTrack', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({audio: true})
|
||
|
.then((stream) => {
|
||
|
const track = stream.getTracks()[0];
|
||
|
pc.addTrack(track, stream);
|
||
|
const senders = pc.getSenders();
|
||
|
expect(senders.length).to.equal(1);
|
||
|
expect(senders[0].track).to.equal(track);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('getReceivers', () => {
|
||
|
let pc;
|
||
|
beforeEach(() => {
|
||
|
pc = new RTCPeerConnection();
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
pc.close();
|
||
|
});
|
||
|
|
||
|
it('returns an empty array initially', () => {
|
||
|
expect(pc.getReceivers().length).to.equal(0);
|
||
|
});
|
||
|
|
||
|
it('returns a single element after SRD with a track', (done) => {
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE +
|
||
|
'a=ssrc:1001 msid:stream1 track1\r\n' +
|
||
|
'a=ssrc:1001 cname:some\r\n';
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
const receivers = pc.getReceivers();
|
||
|
expect(receivers.length).to.equal(1);
|
||
|
expect(receivers[0].track.kind).to.equal('audio');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('getLocalStreams', () => {
|
||
|
let pc;
|
||
|
beforeEach(() => {
|
||
|
pc = new RTCPeerConnection();
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
pc.close();
|
||
|
});
|
||
|
|
||
|
it('returns an empty array initially', () => {
|
||
|
expect(pc.getLocalStreams().length).to.equal(0);
|
||
|
});
|
||
|
|
||
|
describe('returns a single element after', () => {
|
||
|
it('addTrack was called once', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({audio: true})
|
||
|
.then((stream) => {
|
||
|
const track = stream.getTracks()[0];
|
||
|
pc.addTrack(track, stream);
|
||
|
})
|
||
|
.then(() => {
|
||
|
const localStreams = pc.getLocalStreams();
|
||
|
expect(localStreams.length).to.equal(1);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('addTrack was called twice with tracks from the ' +
|
||
|
'same stream', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({audio: true, video: true})
|
||
|
.then((stream) => {
|
||
|
stream.getTracks().forEach(track => {
|
||
|
pc.addTrack(track, stream);
|
||
|
});
|
||
|
})
|
||
|
.then(() => {
|
||
|
const localStreams = pc.getLocalStreams();
|
||
|
expect(localStreams.length).to.equal(1);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('addStream was called', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({audio: true, video: true})
|
||
|
.then((stream) => {
|
||
|
pc.addStream(stream);
|
||
|
})
|
||
|
.then(() => {
|
||
|
const localStreams = pc.getLocalStreams();
|
||
|
expect(localStreams.length).to.equal(1);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('returns two streams after', () => {
|
||
|
it('addTrack was called twice with tracks from two ' +
|
||
|
'streams', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({audio: true})
|
||
|
.then((stream) => {
|
||
|
stream.getTracks().forEach(track => {
|
||
|
pc.addTrack(track, stream);
|
||
|
});
|
||
|
return navigator.mediaDevices.getUserMedia({video: true});
|
||
|
})
|
||
|
.then((stream) => {
|
||
|
stream.getTracks().forEach(track => {
|
||
|
pc.addTrack(track, stream);
|
||
|
});
|
||
|
})
|
||
|
.then(() => {
|
||
|
const localStreams = pc.getLocalStreams();
|
||
|
expect(localStreams.length).to.equal(2);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('addStream was called twice', (done) => {
|
||
|
navigator.mediaDevices.getUserMedia({audio: true})
|
||
|
.then((stream) => {
|
||
|
pc.addStream(stream);
|
||
|
return navigator.mediaDevices.getUserMedia({video: true});
|
||
|
})
|
||
|
.then((stream) => {
|
||
|
pc.addStream(stream);
|
||
|
})
|
||
|
.then(() => {
|
||
|
const localStreams = pc.getLocalStreams();
|
||
|
expect(localStreams.length).to.equal(2);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('getRemoteStreams', () => {
|
||
|
let pc;
|
||
|
beforeEach(() => {
|
||
|
pc = new RTCPeerConnection();
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
pc.close();
|
||
|
});
|
||
|
|
||
|
it('returns an empty array initially', () => {
|
||
|
expect(pc.getRemoteStreams().length).to.equal(0);
|
||
|
});
|
||
|
|
||
|
describe('returns a single element after SRD', () => {
|
||
|
it('with a single track', (done) => {
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE +
|
||
|
'a=ssrc:1001 msid:stream1 track1\r\n' +
|
||
|
'a=ssrc:1001 cname:some\r\n';
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
const remoteStreams = pc.getRemoteStreams();
|
||
|
expect(remoteStreams.length).to.equal(1);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('with two tracks in a single stream', (done) => {
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE +
|
||
|
'a=ssrc:1001 msid:stream1 track1\r\n' +
|
||
|
'a=ssrc:1001 cname:some\r\n' +
|
||
|
MINIMAL_AUDIO_MLINE +
|
||
|
'a=ssrc:1001 msid:stream1 track2\r\n' +
|
||
|
'a=ssrc:1001 cname:some\r\n';
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
const remoteStreams = pc.getRemoteStreams();
|
||
|
expect(remoteStreams.length).to.equal(1);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('returns two streams after SRD', () => {
|
||
|
it('with two tracks in two streams', (done) => {
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE +
|
||
|
'a=ssrc:1001 msid:stream1 track1\r\n' +
|
||
|
'a=ssrc:1001 cname:some\r\n' +
|
||
|
MINIMAL_AUDIO_MLINE +
|
||
|
'a=ssrc:1001 msid:stream2 track1\r\n' +
|
||
|
'a=ssrc:1001 cname:some\r\n';
|
||
|
pc.setRemoteDescription({type: 'offer', sdp: sdp})
|
||
|
.then(() => {
|
||
|
const remoteStreams = pc.getRemoteStreams();
|
||
|
expect(remoteStreams.length).to.equal(2);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('removeTrack', () => {
|
||
|
let pc;
|
||
|
beforeEach(() => {
|
||
|
pc = new RTCPeerConnection();
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
if (pc.signalingState !== 'closed') {
|
||
|
pc.close();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
it('throws a TypeError if the argument is not an RTCRtpSender', () => {
|
||
|
const removeTrack = () => {
|
||
|
pc.removeTrack('something');
|
||
|
};
|
||
|
expect(removeTrack).to.throw(/does not implement/)
|
||
|
.that.has.property('name').that.equals('TypeError');
|
||
|
});
|
||
|
|
||
|
it('throws an InvalidAccessError if the sender does not belong ' +
|
||
|
'to the peerconnection', () => {
|
||
|
const removeTrack = () => {
|
||
|
pc.removeTrack(new window.RTCRtpSender());
|
||
|
};
|
||
|
expect(removeTrack).to.throw(/not created by/)
|
||
|
.that.has.property('name').that.equals('InvalidAccessError');
|
||
|
});
|
||
|
|
||
|
it('throws an InvalidStateError if the peerconnection has been ' +
|
||
|
'closed already', () => {
|
||
|
pc.close();
|
||
|
const removeTrack = () => {
|
||
|
pc.removeTrack(new window.RTCRtpSender());
|
||
|
};
|
||
|
expect(removeTrack).to.throw()
|
||
|
.that.has.property('name').that.equals('InvalidStateError');
|
||
|
});
|
||
|
|
||
|
it('makes the m-line recvonly', () => {
|
||
|
return navigator.mediaDevices.getUserMedia({audio: true})
|
||
|
.then((stream) => {
|
||
|
const sender = pc.addTrack(stream.getAudioTracks()[0], stream);
|
||
|
pc.removeTrack(sender);
|
||
|
return pc.createOffer();
|
||
|
})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections).to.have.length(1);
|
||
|
expect(SDPUtils.getDirection(sections[0])).to.equal('recvonly');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('and getLocalStreams', () => {
|
||
|
it('removes local streams when the last sender has been removed', () => {
|
||
|
return navigator.mediaDevices.getUserMedia({audio: true})
|
||
|
.then((stream) => {
|
||
|
const sender = pc.addTrack(stream.getAudioTracks()[0], stream);
|
||
|
pc.removeTrack(sender);
|
||
|
expect(pc.getLocalStreams()).to.have.length(0);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('keeps the local stream if there is a transceiver to which the ' +
|
||
|
'stream belongs', () => {
|
||
|
return navigator.mediaDevices.getUserMedia({audio: true, video: true})
|
||
|
.then((stream) => {
|
||
|
pc.addTrack(stream.getAudioTracks()[0], stream);
|
||
|
const sender = pc.addTrack(stream.getVideoTracks()[0], stream);
|
||
|
pc.removeTrack(sender);
|
||
|
expect(pc.getLocalStreams()).to.have.length(1);
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('legacy removeStream', () => {
|
||
|
it('removes the stream from getLocalStreams', () => {
|
||
|
return navigator.mediaDevices.getUserMedia({audio: true})
|
||
|
.then((stream) => {
|
||
|
pc.addStream(stream);
|
||
|
pc.removeStream(stream);
|
||
|
expect(pc.getLocalStreams()).to.have.length(0);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('makes the m-line recvonly', () => {
|
||
|
return navigator.mediaDevices.getUserMedia({audio: true})
|
||
|
.then((stream) => {
|
||
|
pc.addStream(stream);
|
||
|
pc.removeStream(stream);
|
||
|
return pc.createOffer();
|
||
|
})
|
||
|
.then((offer) => {
|
||
|
const sections = SDPUtils.getMediaSections(offer.sdp);
|
||
|
expect(sections).to.have.length(1);
|
||
|
expect(SDPUtils.getDirection(sections[0])).to.equal('recvonly');
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('addTrack', () => {
|
||
|
let pc;
|
||
|
beforeEach(() => {
|
||
|
pc = new RTCPeerConnection();
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
pc.close();
|
||
|
});
|
||
|
|
||
|
describe('throws an exception', () => {
|
||
|
it('if the track has already been added', () => {
|
||
|
return navigator.mediaDevices.getUserMedia({audio: true})
|
||
|
.then(stream => {
|
||
|
pc.addTrack(stream.getTracks()[0], stream);
|
||
|
const again = () => {
|
||
|
pc.addTrack(stream.getTracks()[0], stream);
|
||
|
};
|
||
|
expect(again).to.throw(/already/)
|
||
|
.that.has.property('name').that.equals('InvalidAccessError');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('if the track has already been added via addStream', () => {
|
||
|
return navigator.mediaDevices.getUserMedia({audio: true})
|
||
|
.then(stream => {
|
||
|
pc.addStream(stream);
|
||
|
const again = () => {
|
||
|
pc.addTrack(stream.getTracks()[0], stream);
|
||
|
};
|
||
|
expect(again).to.throw(/already/)
|
||
|
.that.has.property('name').that.equals('InvalidAccessError');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('if addStream is called with a stream containing a track ' +
|
||
|
'already added', () => {
|
||
|
return navigator.mediaDevices.getUserMedia({audio: true, video: true})
|
||
|
.then(stream => {
|
||
|
pc.addTrack(stream.getTracks()[0], stream);
|
||
|
const again = () => {
|
||
|
pc.addStream(stream);
|
||
|
};
|
||
|
expect(again).to.throw(/already/)
|
||
|
.that.has.property('name').that.equals('InvalidAccessError');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('if the peerconnection has been closed already', () => {
|
||
|
return navigator.mediaDevices.getUserMedia({audio: true})
|
||
|
.then(stream => {
|
||
|
pc.close();
|
||
|
const afterClose = () => {
|
||
|
pc.addTrack(stream.getTracks()[0], stream);
|
||
|
};
|
||
|
expect(afterClose).to.throw(/closed/)
|
||
|
.that.has.property('name').that.equals('InvalidStateError');
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('getConfiguration', () => {
|
||
|
let pc;
|
||
|
it('fills in default values when no configuration is passed', () => {
|
||
|
// do as jan-ivar says in
|
||
|
// https://github.com/w3c/webrtc-pc/issues/1322#issuecomment-305878881
|
||
|
pc = new RTCPeerConnection();
|
||
|
const config = pc.getConfiguration();
|
||
|
expect(config).to.be.an('Object');
|
||
|
expect(config.bundlePolicy).to.equal('balanced');
|
||
|
expect(config.iceCandidatePoolSize).to.equal(0);
|
||
|
expect(config.iceServers).to.be.an('Array');
|
||
|
expect(config.iceServers.length).equal(0);
|
||
|
expect(config.iceTransportPolicy).to.equal('all');
|
||
|
expect(config.rtcpMuxPolicy).to.equal('require');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('filtering of STUN and TURN servers', () => {
|
||
|
let pc;
|
||
|
|
||
|
it('converts legacy url member to urls', () => {
|
||
|
pc = new RTCPeerConnection({
|
||
|
iceServers: [{url: 'stun:stun.l.google.com'}]
|
||
|
});
|
||
|
const config = pc.getConfiguration();
|
||
|
expect(config.iceServers).to.deep.equal([
|
||
|
{urls: 'stun:stun.l.google.com'}
|
||
|
]);
|
||
|
});
|
||
|
|
||
|
it('filters STUN before r14393', () => {
|
||
|
RTCPeerConnection = shimPeerConnection(window, 14392);
|
||
|
pc = new RTCPeerConnection({
|
||
|
iceServers: [{urls: 'stun:stun.l.google.com'}]
|
||
|
});
|
||
|
const config = pc.getConfiguration();
|
||
|
expect(config.iceServers).to.deep.equal([]);
|
||
|
});
|
||
|
|
||
|
it('does not filter STUN without protocol after r14393', () => {
|
||
|
pc = new RTCPeerConnection({
|
||
|
iceServers: [{urls: 'stun:stun.l.google.com'}]
|
||
|
});
|
||
|
const config = pc.getConfiguration();
|
||
|
expect(config.iceServers).to.deep.equal([
|
||
|
{urls: 'stun:stun.l.google.com'}
|
||
|
]);
|
||
|
});
|
||
|
|
||
|
it('does filter STUN with protocol even after r14393', () => {
|
||
|
pc = new RTCPeerConnection({
|
||
|
iceServers: [{urls: 'stun:stun.l.google.com:19302?transport=udp'}]
|
||
|
});
|
||
|
const config = pc.getConfiguration();
|
||
|
expect(config.iceServers).to.deep.equal([]);
|
||
|
});
|
||
|
|
||
|
it('filters incomplete TURN urls', () => {
|
||
|
pc = new RTCPeerConnection({
|
||
|
iceServers: [
|
||
|
{urls: 'turn:stun.l.google.com'},
|
||
|
{urls: 'turn:stun.l.google.com:19302'}
|
||
|
]
|
||
|
});
|
||
|
const config = pc.getConfiguration();
|
||
|
expect(config.iceServers).to.deep.equal([]);
|
||
|
});
|
||
|
|
||
|
it('filters TURN TCP', () => {
|
||
|
pc = new RTCPeerConnection({
|
||
|
iceServers: [
|
||
|
{urls: 'turn:stun.l.google.com:19302?transport=tcp'}
|
||
|
]
|
||
|
});
|
||
|
const config = pc.getConfiguration();
|
||
|
expect(config.iceServers).to.deep.equal([]);
|
||
|
});
|
||
|
|
||
|
describe('removes all but the first server of a type', () => {
|
||
|
it('in separate entries', () => {
|
||
|
pc = new RTCPeerConnection({
|
||
|
iceServers: [
|
||
|
{urls: 'stun:stun.l.google.com'},
|
||
|
{urls: 'turn:stun.l.google.com:19301?transport=udp'},
|
||
|
{urls: 'turn:stun.l.google.com:19302?transport=udp'}
|
||
|
]
|
||
|
});
|
||
|
const config = pc.getConfiguration();
|
||
|
expect(config.iceServers).to.deep.equal([
|
||
|
{urls: 'stun:stun.l.google.com'},
|
||
|
{urls: 'turn:stun.l.google.com:19301?transport=udp'}
|
||
|
]);
|
||
|
});
|
||
|
|
||
|
it('in urls entries', () => {
|
||
|
pc = new RTCPeerConnection({
|
||
|
iceServers: [
|
||
|
{urls: 'stun:stun.l.google.com'},
|
||
|
{urls: [
|
||
|
'turn:stun.l.google.com:19301?transport=udp',
|
||
|
'turn:stun.l.google.com:19302?transport=udp'
|
||
|
]}
|
||
|
]
|
||
|
});
|
||
|
const config = pc.getConfiguration();
|
||
|
expect(config.iceServers).to.deep.equal([
|
||
|
{urls: 'stun:stun.l.google.com'},
|
||
|
{urls: ['turn:stun.l.google.com:19301?transport=udp']}
|
||
|
]);
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('getStats', () => {
|
||
|
let pc;
|
||
|
beforeEach((done) => {
|
||
|
pc = new RTCPeerConnection();
|
||
|
navigator.mediaDevices.getUserMedia({audio: true})
|
||
|
.then((stream) => {
|
||
|
pc.addTrack(stream.getAudioTracks()[0], stream);
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
pc.close();
|
||
|
});
|
||
|
|
||
|
it('returns a promise', (done) => {
|
||
|
pc.getStats()
|
||
|
.then(() => {
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('calls the legacy success callback', (done) => {
|
||
|
pc.getStats(null, function() {
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('hyphenates stats', () => {
|
||
|
return pc.getStats()
|
||
|
.then(stats => {
|
||
|
let hasOutbound = false;
|
||
|
stats.forEach(stat => hasOutbound |= (stat.type === 'outbound-rtp'));
|
||
|
expect(hasOutbound).to.equal(1); // |= changes to 1.
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('with a track selector', () => {
|
||
|
it('calls getStats on the sender', () => {
|
||
|
const sender = pc.getSenders()[0];
|
||
|
sinon.spy(sender, 'getStats');
|
||
|
return pc.getStats(sender.track)
|
||
|
.then(() => {
|
||
|
expect(sender.getStats).to.have.been.calledOnce();
|
||
|
});
|
||
|
});
|
||
|
it('calls getStats on the receiver', () => {
|
||
|
const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
|
||
|
let receiver;
|
||
|
return pc.setRemoteDescription({type: 'offer', sdp})
|
||
|
.then(() => {
|
||
|
receiver = pc.getReceivers()[0];
|
||
|
sinon.spy(receiver, 'getStats');
|
||
|
return pc.getStats(receiver.track);
|
||
|
})
|
||
|
.then(() => {
|
||
|
expect(receiver.getStats).to.have.been.calledOnce();
|
||
|
});
|
||
|
});
|
||
|
it('throws an InvalidAccessError if the track is not assocіated', () => {
|
||
|
const getStats = () => {
|
||
|
pc.getStats(new window.MediaStreamTrack());
|
||
|
};
|
||
|
expect(getStats).to.throw()
|
||
|
.that.has.property('name').that.equals('InvalidAccessError');
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('RTCIceCandidate contains a port property in', () => {
|
||
|
it('the onicecandidate callback', (done) => {
|
||
|
let hasProperty = false;
|
||
|
const pc = new RTCPeerConnection();
|
||
|
pc.onicecandidate = (e) => {
|
||
|
if (!e.candidate) {
|
||
|
expect(hasProperty).to.equal(true);
|
||
|
done();
|
||
|
} else {
|
||
|
hasProperty = e.candidate.hasOwnProperty('port');
|
||
|
}
|
||
|
};
|
||
|
pc.createOffer({offerToReceiveAudio: true})
|
||
|
.then(offer => pc.setLocalDescription(offer));
|
||
|
});
|
||
|
|
||
|
it('the icecandidate event', (done) => {
|
||
|
let hasProperty = false;
|
||
|
const pc = new RTCPeerConnection();
|
||
|
pc.addEventListener('icecandidate', (e) => {
|
||
|
if (!e.candidate) {
|
||
|
expect(hasProperty).to.equal(true);
|
||
|
done();
|
||
|
} else {
|
||
|
hasProperty = e.candidate.hasOwnProperty('port');
|
||
|
}
|
||
|
});
|
||
|
pc.createOffer({offerToReceiveAudio: true})
|
||
|
.then(offer => pc.setLocalDescription(offer));
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('_updateIceConnectionState', () => {
|
||
|
let pc;
|
||
|
beforeEach(() => {
|
||
|
pc = new RTCPeerConnection();
|
||
|
return pc.createOffer({offerToReceiveAudio: 1});
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
pc.close();
|
||
|
});
|
||
|
|
||
|
it('calls both event and oicenconnectionstatechange', () => {
|
||
|
pc.iceConnectionState = 'weird state';
|
||
|
|
||
|
const stub = sinon.stub();
|
||
|
pc.oniceconnectionstatechange = stub;
|
||
|
pc.addEventListener('iceconnectionstatechange', stub);
|
||
|
|
||
|
pc._updateIceConnectionState();
|
||
|
|
||
|
expect(stub).to.have.been.calledTwice();
|
||
|
expect(pc.iceConnectionState).to.equal('new');
|
||
|
});
|
||
|
|
||
|
describe('emits connectionstatechange when ice is', () => {
|
||
|
['checking', 'connected', 'completed', 'disconnected', 'failed']
|
||
|
.forEach(state => {
|
||
|
it(state, () => {
|
||
|
const transceiver = pc.transceivers[0];
|
||
|
const iceTransport = transceiver.iceTransport;
|
||
|
iceTransport.state = state;
|
||
|
|
||
|
const stub = sinon.stub();
|
||
|
pc.oniceconnectionstatechange = stub;
|
||
|
|
||
|
iceTransport.onicestatechange();
|
||
|
|
||
|
expect(stub).to.have.been.calledOnce();
|
||
|
expect(pc.iceConnectionState).to.equal(state);
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('_updateConnectionState', () => {
|
||
|
let pc;
|
||
|
beforeEach(() => {
|
||
|
pc = new RTCPeerConnection();
|
||
|
return pc.createOffer({offerToReceiveAudio: 1});
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
pc.close();
|
||
|
});
|
||
|
|
||
|
it('calls both event and onconnectionstatechange', () => {
|
||
|
pc.connectionState = 'weird state';
|
||
|
|
||
|
const stub = sinon.stub();
|
||
|
pc.onconnectionstatechange = stub;
|
||
|
pc.addEventListener('connectionstatechange', stub);
|
||
|
|
||
|
pc._updateConnectionState();
|
||
|
|
||
|
expect(stub).to.have.been.calledTwice();
|
||
|
expect(pc.connectionState).to.equal('new');
|
||
|
});
|
||
|
|
||
|
it('does not emit connectionstatechange when just the ' +
|
||
|
'ice connection changes', () => {
|
||
|
const transceiver = pc.transceivers[0];
|
||
|
const iceTransport = transceiver.iceTransport;
|
||
|
iceTransport.state = 'connected';
|
||
|
|
||
|
const stub = sinon.stub();
|
||
|
pc.onconnectionstatechange = stub;
|
||
|
pc.addEventListener('connectionstatechange', stub);
|
||
|
|
||
|
iceTransport.onicestatechange();
|
||
|
expect(stub).not.to.have.been.calledWith();
|
||
|
});
|
||
|
|
||
|
it('emits connectionstatechange when ice and dtls are connected', () => {
|
||
|
const transceiver = pc.transceivers[0];
|
||
|
const iceTransport = transceiver.iceTransport;
|
||
|
iceTransport.state = 'connected';
|
||
|
|
||
|
const dtlsTransport = transceiver.dtlsTransport;
|
||
|
dtlsTransport.state = 'connected';
|
||
|
|
||
|
const stub = sinon.stub();
|
||
|
pc.onconnectionstatechange = stub;
|
||
|
|
||
|
dtlsTransport.ondtlsstatechange();
|
||
|
|
||
|
expect(stub).to.have.been.calledOnce();
|
||
|
expect(pc.connectionState).to.equal('connected');
|
||
|
});
|
||
|
|
||
|
it('changes the connection state to failed when there ' +
|
||
|
'was a DTLS error', () => {
|
||
|
const transceiver = pc.transceivers[0];
|
||
|
const dtlsTransport = transceiver.dtlsTransport;
|
||
|
|
||
|
const stub = sinon.stub();
|
||
|
pc.onconnectionstatechange = stub;
|
||
|
|
||
|
dtlsTransport.onerror();
|
||
|
expect(stub).to.have.been.calledOnce();
|
||
|
expect(pc.connectionState).to.equal('failed');
|
||
|
});
|
||
|
|
||
|
it('changes the connection state to disconnected when the ICE ' +
|
||
|
'connection disconnects', () => {
|
||
|
pc.connectionState = 'connected';
|
||
|
|
||
|
const transceiver = pc.transceivers[0];
|
||
|
const iceTransport = transceiver.iceTransport;
|
||
|
iceTransport.state = 'disconnected';
|
||
|
|
||
|
const dtlsTransport = transceiver.dtlsTransport;
|
||
|
dtlsTransport.state = 'connected';
|
||
|
|
||
|
const stub = sinon.stub();
|
||
|
pc.onconnectionstatechange = stub;
|
||
|
|
||
|
iceTransport.onicestatechange();
|
||
|
|
||
|
expect(stub).to.have.been.calledOnce();
|
||
|
expect(pc.iceConnectionState).to.equal('disconnected');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('edge pre-rtx behaviour', () => {
|
||
|
let pc;
|
||
|
beforeEach(() => {
|
||
|
RTCPeerConnection = shimPeerConnection(window, 15000); // must be < 15019
|
||
|
pc = new RTCPeerConnection();
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
pc.close();
|
||
|
});
|
||
|
|
||
|
it('does not create an offer with RTX', (done) => {
|
||
|
pc.createOffer({offerToReceiveVideo: true})
|
||
|
.then((offer) => {
|
||
|
expect(offer.sdp).not.to.contain(' rtx/90000');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('does not answer with RTX', (done) => {
|
||
|
const sdp = SDP_BOILERPLATE +
|
||
|
'm=video 9 UDP/TLS/RTP/SAVPF 102 103\r\n' +
|
||
|
'c=IN IP4 0.0.0.0\r\n' +
|
||
|
'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
|
||
|
'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
|
||
|
'a=ice-pwd:' + ICEPWD + '\r\n' +
|
||
|
'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
|
||
|
'a=setup:actpass\r\n' +
|
||
|
'a=mid:video1\r\n' +
|
||
|
'a=sendrecv\r\n' +
|
||
|
'a=rtcp-mux\r\n' +
|
||
|
'a=rtcp-rsize\r\n' +
|
||
|
'a=rtpmap:102 vp8/90000\r\n' +
|
||
|
'a=rtpmap:103 rtx/90000\r\n' +
|
||
|
'a=fmtp:103 apt=102\r\n' +
|
||
|
'a=ssrc-group:FID 1001 1002\r\n' +
|
||
|
'a=ssrc:1001 msid:stream1 track1\r\n' +
|
||
|
'a=ssrc:1001 cname:some\r\n' +
|
||
|
'a=ssrc:1002 msid:stream1 track1\r\n' +
|
||
|
'a=ssrc:1002 cname:some\r\n';
|
||
|
navigator.mediaDevices.getUserMedia({video: true})
|
||
|
.then((stream) => {
|
||
|
pc.addTrack(stream.getTracks()[0], stream);
|
||
|
return pc.setRemoteDescription({type: 'offer', sdp});
|
||
|
})
|
||
|
.then(() => {
|
||
|
return pc.createAnswer();
|
||
|
})
|
||
|
.then((answer) => {
|
||
|
expect(answer.sdp).not.to.contain(' rtx/90000');
|
||
|
done();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('non-rtx answer to rtx', () => {
|
||
|
let pc;
|
||
|
beforeEach(() => {
|
||
|
pc = new RTCPeerConnection();
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
pc.close();
|
||
|
});
|
||
|
it('does not call send() with RTX', () => {
|
||
|
let sender;
|
||
|
return navigator.mediaDevices.getUserMedia({video: true})
|
||
|
.then((stream) => {
|
||
|
sender = pc.addTrack(stream.getTracks()[0], stream);
|
||
|
sender.send = sinon.stub();
|
||
|
})
|
||
|
.then(() => pc.createOffer())
|
||
|
.then((offer) => pc.setLocalDescription(offer))
|
||
|
.then(() => {
|
||
|
const localMid = SDPUtils.getMid(
|
||
|
SDPUtils.splitSections(pc.localDescription.sdp)[1]);
|
||
|
const candidateString = 'a=candidate:702786350 1 udp 41819902 ' +
|
||
|
'8.8.8.8 60769 typ host';
|
||
|
const sdp = 'v=0\r\n' +
|
||
|
'o=- 0 0 IN IP4 127.0.0.1\r\n' +
|
||
|
's=nortxanswer\r\n' +
|
||
|
't=0 0\r\n' +
|
||
|
'm=video 1 UDP/TLS/RTP/SAVPF 100\r\n' +
|
||
|
'c=IN IP4 0.0.0.0\r\n' +
|
||
|
'a=rtpmap:100 VP8/90000\r\n' +
|
||
|
'a=rtcp:1 IN IP4 0.0.0.0\r\n' +
|
||
|
'a=rtcp-fb:100 nack\r\n' +
|
||
|
'a=rtcp-fb:100 nack pli\r\n' +
|
||
|
'a=rtcp-fb:100 goog-remb\r\n' +
|
||
|
'a=extmap:1 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n' +
|
||
|
'a=setup:active\r\n' +
|
||
|
'a=mid:' + localMid + '\r\n' +
|
||
|
'a=recvonly\r\n' +
|
||
|
'a=ice-ufrag:S5Zq\r\n' +
|
||
|
'a=ice-pwd:6E1muhzVwnphsbN6uokNU/\r\n' +
|
||
|
'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
|
||
|
candidateString + '\r\n' +
|
||
|
'a=end-of-candidates\r\n' +
|
||
|
'a=rtcp-mux\r\n';
|
||
|
return pc.setRemoteDescription({type: 'answer', sdp});
|
||
|
})
|
||
|
.then(() => {
|
||
|
expect(sender.send).to.have.been.calledWith(
|
||
|
sinon.match.has('encodings', [{ssrc: 1001}]));
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('edge clonestream issue', () => {
|
||
|
let pc;
|
||
|
beforeEach(() => {
|
||
|
RTCPeerConnection = shimPeerConnection(window, 15000); // must be < 15025
|
||
|
pc = new RTCPeerConnection();
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
pc.close();
|
||
|
});
|
||
|
|
||
|
it('clones the stream before addStream', () => {
|
||
|
navigator.mediaDevices.getUserMedia({video: true})
|
||
|
.then((stream) => {
|
||
|
stream.clone = sinon.stub().returns(stream);
|
||
|
pc.addStream(stream);
|
||
|
expect(stream.clone).to.have.been.calledOnce();
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|