https://github.com/web-platform-tests/wpt
Tip revision: c4742caa9ad8c4b4a00ded7c4dac2a1e1563cb4b authored by L. David Baron on 02 April 2018, 19:01:11 UTC
Sync Mozilla tests as of https://hg.mozilla.org/mozilla-central/rev/c44f60c43432d468639b5fe078420e60c13fd3de .
Sync Mozilla tests as of https://hg.mozilla.org/mozilla-central/rev/c44f60c43432d468639b5fe078420e60c13fd3de .
Tip revision: c4742ca
RTCPeerConnection-helper.js
'use strict'
/*
* Helper Methods for testing the following methods in RTCPeerConnection:
* createOffer
* createAnswer
* setLocalDescription
* setRemoteDescription
*
* This file offers the following features:
* SDP similarity comparison
* Generating offer/answer using anonymous peer connection
* Test signalingstatechange event
* Test promise that never resolve
*/
const audioLineRegex = /\r\nm=audio.+\r\n/g;
const videoLineRegex = /\r\nm=video.+\r\n/g;
const applicationLineRegex = /\r\nm=application.+\r\n/g;
function countLine(sdp, regex) {
const matches = sdp.match(regex);
if(matches === null) {
return 0;
} else {
return matches.length;
}
}
function countAudioLine(sdp) {
return countLine(sdp, audioLineRegex);
}
function countVideoLine(sdp) {
return countLine(sdp, videoLineRegex);
}
function countApplicationLine(sdp) {
return countLine(sdp, applicationLineRegex);
}
function similarMediaDescriptions(sdp1, sdp2) {
if(sdp1 === sdp2) {
return true;
} else if(
countAudioLine(sdp1) !== countAudioLine(sdp2) ||
countVideoLine(sdp1) !== countVideoLine(sdp2) ||
countApplicationLine(sdp1) !== countApplicationLine(sdp2))
{
return false;
} else {
return true;
}
}
// Assert that given object is either an
// RTCSessionDescription or RTCSessionDescriptionInit
function assert_is_session_description(sessionDesc) {
if(sessionDesc instanceof RTCSessionDescription) {
return;
}
assert_not_equals(sessionDesc, undefined,
'Expect session description to be defined');
assert_true(typeof(sessionDesc) === 'object',
'Expect sessionDescription to be either a RTCSessionDescription or an object');
assert_true(typeof(sessionDesc.type) === 'string',
'Expect sessionDescription.type to be a string');
assert_true(typeof(sessionDesc.sdp) === 'string',
'Expect sessionDescription.sdp to be a string');
}
// We can't do string comparison to the SDP content,
// because RTCPeerConnection may return SDP that is
// slightly modified or reordered from what is given
// to it due to ICE candidate events or serialization.
// Instead, we create SDP with different number of media
// lines, and if the SDP strings are not the same, we
// simply count the media description lines and if they
// are the same, we assume it is the same.
function isSimilarSessionDescription(sessionDesc1, sessionDesc2) {
assert_is_session_description(sessionDesc1);
assert_is_session_description(sessionDesc2);
if(sessionDesc1.type !== sessionDesc2.type) {
return false;
} else {
return similarMediaDescriptions(sessionDesc1.sdp, sessionDesc2.sdp);
}
}
function assert_session_desc_equals(sessionDesc1, sessionDesc2) {
assert_true(isSimilarSessionDescription(sessionDesc1, sessionDesc2),
'Expect both session descriptions to have the same count of media lines');
}
function assert_session_desc_not_equals(sessionDesc1, sessionDesc2) {
assert_false(isSimilarSessionDescription(sessionDesc1, sessionDesc2),
'Expect both session descriptions to have different count of media lines');
}
// Helper function to generate offer using a freshly created RTCPeerConnection
// object with any audio, video, data media lines present
function generateOffer(options={}) {
const {
audio = false,
video = false,
data = false,
pc,
} = options;
if (data) {
pc.createDataChannel('test');
}
const setup = {};
if (audio) {
setup.offerToReceiveAudio = true;
}
if (video) {
setup.offerToReceiveVideo = true;
}
return pc.createOffer(setup).then(offer => {
// Guard here to ensure that the generated offer really
// contain the number of media lines we want
const { sdp } = offer;
if(audio) {
assert_equals(countAudioLine(sdp), 1,
'Expect m=audio line to be present in generated SDP');
} else {
assert_equals(countAudioLine(sdp), 0,
'Expect m=audio line to be present in generated SDP');
}
if(video) {
assert_equals(countVideoLine(sdp), 1,
'Expect m=video line to be present in generated SDP');
} else {
assert_equals(countVideoLine(sdp), 0,
'Expect m=video line to not present in generated SDP');
}
if(data) {
assert_equals(countApplicationLine(sdp), 1,
'Expect m=application line to be present in generated SDP');
} else {
assert_equals(countApplicationLine(sdp), 0,
'Expect m=application line to not present in generated SDP');
}
return offer;
});
}
// Helper function to generate answer based on given offer using a freshly
// created RTCPeerConnection object
function generateAnswer(offer) {
const pc = new RTCPeerConnection();
return pc.setRemoteDescription(offer)
.then(() => pc.createAnswer());
}
// Wait for peer connection to fire onsignalingstatechange
// event, compare and make sure the new state is the same
// as expected state. It accepts an RTCPeerConnection object
// and an array of expected state changes. The test passes
// if all expected state change events have been fired, and
// fail if the new state is different from the expected state.
//
// Note that the promise is never resolved if no change
// event is fired. To avoid confusion with the main test
// getting timed out, this is done in parallel as a separate
// test
function test_state_change_event(parentTest, pc, expectedStates) {
return async_test(t => {
pc.onsignalingstatechange = t.step_func(() => {
if(expectedStates.length === 0) {
return;
}
const newState = pc.signalingState;
const expectedState = expectedStates.shift();
assert_equals(newState, expectedState, 'New signaling state is different from expected.');
if(expectedStates.length === 0) {
t.done();
}
});
}, `Test onsignalingstatechange event for ${parentTest.name}`);
}
// Run a test function that return a promise that should
// never be resolved. For lack of better options,
// we wait for a time out and pass the test if the
// promise doesn't resolve within that time.
function test_never_resolve(testFunc, testName) {
async_test(t => {
testFunc(t)
.then(
t.step_func(result => {
assert_unreached(`Pending promise should never be resolved. Instead it is fulfilled with: ${result}`);
}),
t.step_func(err => {
assert_unreached(`Pending promise should never be resolved. Instead it is rejected with: ${err}`);
}));
t.step_timeout(t.step_func_done(), 100)
}, testName);
}
// Helper function to exchange ice candidates between
// two local peer connections
function exchangeIceCandidates(pc1, pc2) {
// private function
function doExchange(localPc, remotePc) {
localPc.addEventListener('icecandidate', event => {
const { candidate } = event;
// candidate may be null to indicate end of candidate gathering.
// There is ongoing discussion on w3c/webrtc-pc#1213
// that there should be an empty candidate string event
// for end of candidate for each m= section.
if(candidate) {
remotePc.addIceCandidate(candidate);
}
});
}
doExchange(pc1, pc2);
doExchange(pc2, pc1);
}
// Helper function for doing one round of offer/answer exchange
// betweeen two local peer connections
function doSignalingHandshake(localPc, remotePc) {
return localPc.createOffer()
.then(offer => Promise.all([
localPc.setLocalDescription(offer),
remotePc.setRemoteDescription(offer)]))
.then(() => remotePc.createAnswer())
.then(answer => Promise.all([
remotePc.setLocalDescription(answer),
localPc.setRemoteDescription(answer)]))
}
// Helper function to create a pair of connected data channel.
// On success the promise resolves to an array with two data channels.
// It does the heavy lifting of performing signaling handshake,
// ICE candidate exchange, and waiting for data channel at two
// end points to open.
function createDataChannelPair(
pc1=new RTCPeerConnection(),
pc2=new RTCPeerConnection())
{
const channel1 = pc1.createDataChannel('');
exchangeIceCandidates(pc1, pc2);
return new Promise((resolve, reject) => {
let channel2;
let opened1 = false;
let opened2 = false;
function onBothOpened() {
resolve([channel1, channel2]);
}
function onOpen1() {
opened1 = true;
if(opened2) onBothOpened();
}
function onOpen2() {
opened2 = true;
if(opened1) onBothOpened();
}
function onDataChannel(event) {
channel2 = event.channel;
channel2.addEventListener('error', reject);
const { readyState } = channel2;
if(readyState === 'open') {
onOpen2();
} else if(readyState === 'connecting') {
channel2.addEventListener('open', onOpen2);
} else {
reject(new Error(`Unexpected ready state ${readyState}`));
}
}
channel1.addEventListener('open', onOpen1);
channel1.addEventListener('error', reject);
pc2.addEventListener('datachannel', onDataChannel);
doSignalingHandshake(pc1, pc2);
});
}
// Wait for a single message event and return
// a promise that resolve when the event fires
function awaitMessage(channel) {
return new Promise((resolve, reject) => {
channel.addEventListener('message',
event => resolve(event.data),
{ once: true });
channel.addEventListener('error', reject, { once: true });
});
}
// Helper to convert a blob to array buffer so that
// we can read the content
function blobToArrayBuffer(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener('load', () => {
resolve(reader.result);
});
reader.addEventListener('error', reject);
reader.readAsArrayBuffer(blob);
});
}
// Assert that two ArrayBuffer objects have the same byte values
function assert_equals_array_buffer(buffer1, buffer2) {
assert_true(buffer1 instanceof ArrayBuffer,
'Expect buffer to be instance of ArrayBuffer');
assert_true(buffer2 instanceof ArrayBuffer,
'Expect buffer to be instance of ArrayBuffer');
assert_equals(buffer1.byteLength, buffer2.byteLength,
'Expect both array buffers to be of the same byte length');
const byteLength = buffer1.byteLength;
const byteArray1 = new Uint8Array(buffer1);
const byteArray2 = new Uint8Array(buffer2);
for(let i=0; i<byteLength; i++) {
assert_equals(byteArray1[i], byteArray2[i],
`Expect byte at buffer position ${i} to be equal`);
}
}
// Generate a MediaStreamTrack for testing use.
// We generate it by creating an anonymous RTCPeerConnection,
// call addTransceiver(), and use the remote track
// from RTCRtpReceiver. This track is supposed to
// receive media from a remote peer and be played locally.
// We use this approach instead of getUserMedia()
// to bypass the permission dialog and fake media devices,
// as well as being able to generate many unique tracks.
function generateMediaStreamTrack(kind) {
const pc = new RTCPeerConnection();
assert_idl_attribute(pc, 'addTransceiver',
'Expect pc to have addTransceiver() method');
const transceiver = pc.addTransceiver(kind);
const { receiver } = transceiver;
const { track } = receiver;
assert_true(track instanceof MediaStreamTrack,
'Expect receiver track to be instance of MediaStreamTrack');
return track;
}
// Obtain a MediaStreamTrack of kind using getUserMedia.
// Return Promise of pair of track and associated mediaStream.
// Assumes that there is at least one available device
// to generate the track.
function getTrackFromUserMedia(kind) {
return navigator.mediaDevices.getUserMedia({ [kind]: true })
.then(mediaStream => {
const tracks = mediaStream.getTracks();
assert_greater_than(tracks.length, 0,
`Expect getUserMedia to return at least one track of kind ${kind}`);
const [ track ] = tracks;
return [track, mediaStream];
});
}
// Obtain |count| MediaStreamTracks of type |kind| and MediaStreams. The tracks
// do not belong to any stream and the streams are empty. Returns a Promise
// resolved with a pair of arrays [tracks, streams].
// Assumes there is at least one available device to generate the tracks and
// streams and that the getUserMedia() calls resolve.
function getUserMediaTracksAndStreams(count, type = 'audio') {
let otherTracksPromise;
if (count > 1)
otherTracksPromise = getUserMediaTracksAndStreams(count - 1, type);
else
otherTracksPromise = Promise.resolve([[], []]);
return otherTracksPromise.then(([tracks, streams]) => {
return getTrackFromUserMedia(type)
.then(([track, stream]) => {
// Remove the default stream-track relationship.
stream.removeTrack(track);
tracks.push(track);
streams.push(stream);
return [tracks, streams];
});
});
}
// Creates an offer for the caller, set it as the caller's local description and
// then sets the callee's remote description to the offer. Returns the Promise
// of the setRemoteDescription call.
function performOffer(caller, callee) {
let sessionDescription;
return caller.createOffer()
.then(offer => {
sessionDescription = offer;
return caller.setLocalDescription(offer);
}).then(() => callee.setRemoteDescription(sessionDescription));
}
// The resolver has a |promise| that can be resolved or rejected using |resolve|
// or |reject|.
class Resolver {
constructor() {
let promiseResolve;
let promiseReject;
this.promise = new Promise(function(resolve, reject) {
promiseResolve = resolve;
promiseReject = reject;
});
this.resolve = promiseResolve;
this.reject = promiseReject;
}
}