<html>
<head>
<script type="text/javascript" src="webrtc_codec_utils.js"></script>
<script type="text/javascript" src="webrtc_test_utilities.js"></script>
<script type="text/javascript" src="webrtc_test_common.js"></script>
<script type="text/javascript">
$ = function(id) {
return document.getElementById(id);
};
var gFirstConnection = null;
var gSecondConnection = null;
var gLocalStream = null;
var gRemoteStreams = {};
function call(constraints) {
createConnections();
return Promise.all([
navigator.mediaDevices.getUserMedia(constraints)
.then(addStreamToBothConnectionsAndNegotiate),
detectVideoPlaying('remote-view-1'),
detectVideoPlaying('remote-view-2')
])
.then(logSuccess);
}
function oldStyleCall() {
createConnections();
return Promise.all([
new Promise((resolve, reject) => {
navigator.webkitGetUserMedia({video: true, audio: true},
resolve, reject);
}).then(addStreamToBothConnectionsAndNegotiate),
detectVideoPlaying('remote-view-1'),
detectVideoPlaying('remote-view-2')
])
.then(logSuccess);
}
function hangup() {
gFirstConnection.close();
gSecondConnection.close();
gFirstConnection = null;
gSecondConnection = null;
gLocalStream.getTracks().forEach(function(track) {
track.stop();
});
gLocalStream = null;
return logSuccess();
}
function callAndDisableLocalVideo(constraints) {
createConnections();
return Promise.all([
navigator.mediaDevices.getUserMedia(constraints)
.then(addStreamToBothConnectionsAndNegotiate),
detectVideoPlaying('remote-view-1').then(() => {
assertEquals(gLocalStream.getVideoTracks().length, 1);
gLocalStream.getVideoTracks()[0].enabled = false;
return detectBlackVideo('remote-view-1');
}),
])
.then(logSuccess);
}
function callAndExpectResolution(constraints,
expected_width,
expected_height,
alignment) {
createConnections();
return Promise.all([
navigator.mediaDevices.getUserMedia(constraints)
.then(addStreamToBothConnectionsAndNegotiate),
detectVideoPlayingWithExpectedResolution(
'remote-view-1', expected_width, expected_height, alignment),
detectVideoPlayingWithExpectedResolution(
'remote-view-2', expected_width, expected_height, alignment)
])
.then(logSuccess);
}
function callEmptyThenAddOneStreamAndRenegotiate(constraints) {
createConnections();
return Promise.all([
negotiate(),
waitForConnectionToStabilize(gFirstConnection).then(() => {
return navigator.mediaDevices.getUserMedia(constraints)
.then(addStreamToTheFirstConnectionAndNegotiate);
}).then(() => {
return detectVideoPlaying('remote-view-2');
}),
])
.then(logSuccess);
}
function callAndRenegotiateToVideo(constraints, renegotiationConstraints) {
createConnections();
return Promise.all([
navigator.mediaDevices.getUserMedia(constraints)
.then(addStreamToBothConnectionsAndNegotiate),
waitForConnectionToStabilize(gFirstConnection).then(() => {
gFirstConnection.removeStream(gLocalStream);
gSecondConnection.removeStream(gLocalStream);
}).then(() => {
return navigator.mediaDevices.getUserMedia(renegotiationConstraints)
.then(addStreamToBothConnectionsAndNegotiate);
}).then(() => {
return Promise.all([
detectVideoPlaying('remote-view-1'),
detectVideoPlaying('remote-view-1')]);
}),
])
.then(logSuccess);
}
function callAndForwardRemoteStream(constraints) {
createConnections();
return Promise.all([
navigator.mediaDevices.getUserMedia(constraints)
.then(addStreamToTheFirstConnectionAndNegotiate),
console.log('Initial setup done. Waiting.'),
detectVideoPlaying('remote-view-2').then(() => {
console.log('callAndForward: Adding return stream');
gSecondConnection.addStream(gRemoteStreams['remote-view-2'].clone());
return Promise.all([
negotiate(),
detectVideoPlaying('remote-view-1'),
]);
}),
])
.then(logSuccess);
}
function ConnectChromiumSinkToRemoteAudioTrack() {
createConnections();
return Promise.all([
navigator.mediaDevices.getUserMedia({audio: true, video: true})
.then(addStreamToBothConnectionsAndNegotiate),
detectVideoPlaying('remote-view-2').then(() => {
var newStream = new MediaStream();
newStream.addTrack(
gSecondConnection.getRemoteStreams()[0].getAudioTracks()[0]);
newStream.addTrack(
gSecondConnection.getRemoteStreams()[0].getVideoTracks()[0]);
var videoElement = document.createElement('video');
videoElement.srcObject = newStream;
return detectVideoPlaying('remote-view-2')
}),
])
.then(logSuccess);
}
function callWithoutMsidAndBundle() {
createConnections();
setOfferSdpTransform(removeBundle);
setRemoteSdpTransform(removeMsid);
return Promise.all([
navigator.mediaDevices.getUserMedia({audio: true, video: true})
.then(addStreamToBothConnectionsAndNegotiate),
detectVideoPlaying('remote-view-1'),
detectVideoPlaying('remote-view-1')
])
.then(logSuccess);
}
function negotiateUnsupportedVideoCodec() {
createConnections();
setOfferSdpTransform(removeVideoCodec);
return navigator.mediaDevices.getUserMedia({audio: true, video: true})
.then(addStreamToBothConnectionsAndNegotiate)
.then(
() => {
throw new Error('Expected to fail, but succeeded');
},
(error) => {
var expectedMsg = "Failed to set local offer sdp: Failed to set local " +
"video description recv parameters for m-section with mid='1'.";
assertEquals(expectedMsg, error.message);
return logSuccess();
});
}
function negotiateNonCryptoCall() {
createConnections();
setOfferSdpTransform(removeCrypto);
return navigator.mediaDevices.getUserMedia({audio: true, video: true})
.then(addStreamToBothConnectionsAndNegotiate)
.then(() => {
throw new Error('Expected to fail, but succeeded');
}, (error) => {
var expectedError = 'InvalidAccessError';
assertEquals(expectedError, error.name);
return logSuccess();
});
}
function negotiateOfferWithBLine() {
createConnections();
setOfferSdpTransform(addBandwithControl);
return Promise.all([
navigator.mediaDevices.getUserMedia({audio: true, video: true})
.then(addStreamToBothConnectionsAndNegotiate),
detectVideoPlaying('remote-view-1'),
detectVideoPlaying('remote-view-1')
])
.then(logSuccess);
}
function callAndSendDtmf(tones) {
createConnections();
return Promise.all([
navigator.mediaDevices.getUserMedia({audio: true, video: true})
.then(addStreamToBothConnectionsAndNegotiate),
detectVideoPlaying('remote-view-2').then(() => {
var track = gLocalStream.getAudioTracks()[0];
window.sender = gFirstConnection.createDTMFSender(track);
var receivedTones = '';
return new Promise(resolve => {
window.sender.ontonechange = tone => {
receivedTones += tone.tone;
if (receivedTones == tones) {
resolve();
}
};
window.sender.insertDTMF(tones);
});
}),
])
.then(logSuccess);
}
function testCreateOfferOptions() {
createConnections();
var offerOptions = {
'offerToReceiveAudio': false,
'offerToReceiveVideo': true
};
return gFirstConnection.createOffer(offerOptions)
.then(function(offer) {
assertEquals(-1, offer.sdp.search('m=audio'));
assertNotEquals(-1, offer.sdp.search('m=video'));
return logSuccess();
});
}
function enableRemoteVideo(peerConnection, enabled) {
remoteStream = peerConnection.getRemoteStreams()[0];
remoteStream.getVideoTracks()[0].enabled = enabled;
}
function enableRemoteAudio(peerConnection, enabled) {
remoteStream = peerConnection.getRemoteStreams()[0];
remoteStream.getAudioTracks()[0].enabled = enabled;
}
function callAndEnsureVideoTrackMutingWorks() {
createConnections();
return Promise.all([
navigator.mediaDevices.getUserMedia({audio: true, video: true})
.then(addStreamToBothConnectionsAndNegotiate),
detectVideoPlaying('remote-view-2').then(() => {
enableRemoteVideo(gSecondConnection, false);
enableRemoteAudio(gSecondConnection, false);
return detectVideoStopped('remote-view-2').then(() => {
enableRemoteVideo(gSecondConnection, true);
return detectVideoPlaying('remote-view-2')
})
}),
])
.then(logSuccess);
}
function callWithNewVideoMediaStream() {
createConnections();
return Promise.all([
navigator.mediaDevices.getUserMedia({audio: true, video: true})
.then(createNewVideoStreamAndAddToBothConnections),
detectVideoPlaying('remote-view-1'),
detectVideoPlaying('remote-view-1')
])
.then(logSuccess);
}
function callInsideIframe(constraints) {
return createIframe().then(function(iframe) {
return iframe.contentWindow.call(constraints);
});
}
function createIframe() {
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
return new Promise(resolve => {
iframe.onload = onIframeLoaded;
iframe.src = window.location;
function onIframeLoaded() {
var iframe = window.document.querySelector('iframe');
resolve(iframe);
}
});
}
function addTwoMediaStreamsToOneConnection() {
createConnections();
var called = 0;
var remoteAddStreamCalledTwice = new Promise((resolve, reject) => {
gSecondConnection.onaddstream = () => {
called += 1;
if (called == 2)
resolve();
};
});
function verifyHasOneAudioAndVideoTrack(stream) {
assertEquals(1, stream.getAudioTracks().length);
assertEquals(1, stream.getVideoTracks().length);
}
return navigator.mediaDevices.getUserMedia({audio: true, video: true})
.then((localStream) => {
displayAndRemember(localStream);
var clonedStream = null;
if (typeof localStream.clone === "function") {
clonedStream = localStream.clone();
} else {
clonedStream = new MediaStream(localStream);
}
gFirstConnection.addStream(localStream);
gFirstConnection.addStream(clonedStream);
assertEquals(2, gFirstConnection.getLocalStreams().length);
verifyHasOneAudioAndVideoTrack(gFirstConnection.getLocalStreams()[0]);
verifyHasOneAudioAndVideoTrack(gFirstConnection.getLocalStreams()[1]);
return negotiate();
})
.then(() => { return remoteAddStreamCalledTwice; })
.then(() => {
assertEquals(2, gSecondConnection.getRemoteStreams().length);
verifyHasOneAudioAndVideoTrack(
gSecondConnection.getRemoteStreams()[0]);
verifyHasOneAudioAndVideoTrack(
gSecondConnection.getRemoteStreams()[1]);
})
.then(logSuccess);
}
function createConnections() {
gFirstConnection = createConnection('remote-view-1');
assertEquals('stable', gFirstConnection.signalingState);
gSecondConnection = createConnection('remote-view-2');
assertEquals('stable', gSecondConnection.signalingState);
}
function createConnection(remoteView) {
var pc = new RTCPeerConnection();
pc.onaddstream = function(event) {
onRemoteStream(event, remoteView);
}
return pc;
}
function displayAndRemember(localStream) {
$('local-view').srcObject = localStream;
gLocalStream = localStream;
}
function addStreamToBothConnectionsAndNegotiate(localStream) {
displayAndRemember(localStream);
gFirstConnection.addStream(localStream);
gSecondConnection.addStream(localStream);
return negotiate();
}
function addStreamToTheFirstConnectionAndNegotiate(localStream) {
displayAndRemember(localStream);
gFirstConnection.addStream(localStream);
negotiate();
}
function createNewVideoStreamAndAddToBothConnections(localStream) {
displayAndRemember(localStream);
var newStream = new MediaStream();
newStream.addTrack(localStream.getVideoTracks()[0]);
gFirstConnection.addStream(newStream);
gSecondConnection.addStream(newStream);
return negotiate();
}
function negotiate() {
return negotiateBetween(gFirstConnection, gSecondConnection);
}
function iceCandidateIsLoopback(candidate) {
return candidate.candidate.indexOf("127.0.0.1") > -1 ||
candidate.candidate.indexOf(" ::1 ") > -1;
}
function gatherIceCandidates(pc) {
var candidates = [];
pc.createDataChannel("");
return new Promise((resolve, reject) => {
pc.onicecandidate = function(event) {
if (event.candidate == null) {
resolve(candidates);
} else {
candidates.push(event.candidate);
}
}
pc.createOffer(
function(offer) {
pc.setLocalDescription(offer);
},
function(error) { reject(error); }
);
});
}
function callWithDevicePermissionGranted() {
var pc = new RTCPeerConnection();
return gatherIceCandidates(pc)
.then(function(candidates) {
var hasLoopbackCandidate = false;
assertEquals(candidates.length > 0, true);
for (i = 0; i < candidates.length; i++) {
hasLoopbackCandidate |= iceCandidateIsLoopback(candidates[i]);
}
if (hasLoopbackCandidate) {
return logSuccess();
} else {
throw new Error('expect to see non-default host interface');
}
});
}
function callWithNoCandidateExpected() {
var pc = new RTCPeerConnection();
return gatherIceCandidates(pc)
.then(function(candidates) {
assertEquals(candidates.length, 0);
return logSuccess();
});
}
function callAndExpectNonLoopbackCandidates() {
var pc = new RTCPeerConnection();
return gatherIceCandidates(pc)
.then(function(candidates) {
var hasCandidate = false;
assertEquals(candidates.length > 0, true);
for (i = 0; i < candidates.length; i++) {
hasCandidate = true;
assertEquals(iceCandidateIsLoopback(candidates[i]), false);
}
assertTrue(hasCandidate, 'expect to see at least one candidate');
return logSuccess();
});
}
function removeMsid(offerSdp) {
offerSdp = offerSdp.replace(/a=msid-semantic.*\r\n/g, '');
offerSdp = offerSdp.replace('a=mid:audio\r\n', '');
offerSdp = offerSdp.replace('a=mid:video\r\n', '');
offerSdp = offerSdp.replace(/a=ssrc.*\r\n/g, '');
return offerSdp;
}
function removeVideoCodec(offerSdp) {
offerSdp = offerSdp.replace(/a=rtpmap:(\d+)\ VP8\/90000\r\n/,
'a=rtpmap:$1 XVP8/90000\r\n');
return offerSdp;
}
function removeCrypto(offerSdp) {
offerSdp = offerSdp.replace(/a=crypto.*\r\n/g, 'a=Xcrypto\r\n');
offerSdp = offerSdp.replace(/a=fingerprint.*\r\n/g, '');
return offerSdp;
}
function addBandwithControl(offerSdp) {
offerSdp = offerSdp.replace('a=mid:audio\r\n', 'a=mid:audio\r\n'+
'b=AS:16\r\n');
offerSdp = offerSdp.replace('a=mid:video\r\n', 'a=mid:video\r\n'+
'b=AS:512\r\n');
return offerSdp;
}
function removeBundle(sdp) {
return sdp.replace(/a=group:BUNDLE .*\r\n/g, '');
}
function onRemoteStream(e, target) {
console.log("Receiving remote stream...");
gRemoteStreams[target] = e.stream;
var remoteVideo = $(target);
remoteVideo.srcObject = e.stream;
}
function testApplyConstraints() {
createConnections();
return Promise.all([
navigator.mediaDevices.getUserMedia({audio: true, video: true})
.then(addStreamToBothConnectionsAndNegotiate),
detectVideoPlaying('remote-view-1').then(() => {
var track = gFirstConnection.getRemoteStreams()[0].getVideoTracks()[0];
return track.applyConstraints({width: {min: 1000}})
.then(() => {
throw 'applyConstraints should fail for remote tracks';
}, e => {
assertEquals(e.name, 'OverconstrainedError');
});
}),
])
.then(logSuccess);
}
function testGetSettingsReportsValuesForRemoteTracks() {
createConnections();
return Promise.all([
navigator.mediaDevices.getUserMedia({audio: true, video: true})
.then(addStreamToBothConnectionsAndNegotiate),
detectVideoPlaying('remote-view-1').then(() => {
var settings1 = gFirstConnection.getRemoteStreams()[0].getVideoTracks()[0]
.getSettings();
assertTrue(settings1.width != undefined);
assertTrue(settings1.height != undefined);
assertTrue(settings1.frameRate != undefined);
return detectVideoPlaying('remote-view-2').then(() => {
var settings2 = gSecondConnection.getRemoteStreams()[0]
.getVideoTracks()[0].getSettings();
assertTrue(settings2.width != undefined);
assertTrue(settings2.height != undefined);
assertTrue(settings2.frameRate != undefined);
});
}),
])
.then(logSuccess);
}
async function testEstablishVideoOnlyCallAndVerifyGetSynchronizationSourcesWorks() {
const startTime = performance.timeOrigin + performance.now();
createConnections();
const stream = await navigator.mediaDevices.getUserMedia({video: true});
await addStreamToBothConnectionsAndNegotiate(stream);
await detectVideoPlaying('remote-view-2');
const peerConnection = gSecondConnection;
const receivers = peerConnection.getReceivers();
assertEquals(receivers.length, 1);
const receiver = receivers[0];
assertEquals(receiver.track.kind, 'video');
const results = receiver.getSynchronizationSources();
assertEquals(results.length, 1);
const result = results[0];
const endTime = performance.timeOrigin + performance.now();
console.log('getSynchronizationSources() = ' + JSON.stringify(result));
assertEquals(typeof result.timestamp, 'number');
assertTrue(result.timestamp >= startTime);
assertTrue(result.timestamp <= endTime);
assertEquals(typeof result.source, 'number');
assertTrue(result.source >= 0);
assertTrue(result.source <= 0xffffffff);
assertEquals(typeof result.rtpTimestamp, 'number');
assertTrue(result.rtpTimestamp >= 0);
assertTrue(result.rtpTimestamp <= 0xffffffff);
assertEquals(result.audioLevel, undefined);
assertEquals(result.voiceActivityFlag, undefined);
return logSuccess();
}
async function CanSetupH264VideoCallOnSupportedDevice() {
createConnections();
setOfferSdpTransform(maybePreferH264SendCodec);
await Promise.all([
navigator.mediaDevices.getUserMedia({audio: false, video: true})
.then(addStreamToTheFirstConnectionAndNegotiate),
detectVideoPlaying('remote-view-2'),
]);
assertEquals(gLocalStream.getVideoTracks().length, 1);
return gFirstConnection.getStats(gLocalStream.getVideoTracks()[0])
.then(function(report) {
var numCodecs = 0;
var hasH264 = false;
var supported_codecs = [];
report.forEach(stats => {
if (stats.type == 'codec') {
numCodecs++;
supported_codecs.push(stats.mimeType);
if (stats.mimeType == 'video/H264') {
hasH264 = true;
}
}
});
assertEquals(1, numCodecs);
assertTrue(hasH264, JSON.stringify(supported_codecs));
return logSuccess();
});
}
</script>
</head>
<body>
<table border="0">
<tr>
<td><video width="320" height="240" id="local-view" style="display:none"
autoplay muted></video></td>
<td><video width="320" height="240" id="remote-view-1"
style="display:none" autoplay></video></td>
<td><video width="320" height="240" id="remote-view-2"
style="display:none" autoplay></video></td>
<td><video width="320" height="240" id="remote-view-3"
style="display:none" autoplay></video></td>
<td><video width="320" height="240" id="remote-view-4"
style="display:none" autoplay></video></td>
<td><canvas width="320" height="240" id="remote-view-1-canvas"
style="display:none"></canvas></td>
<td><canvas width="320" height="240" id="remote-view-2-canvas"
style="display:none"></canvas></td>
<td><canvas width="320" height="240" id="remote-view-3-canvas"
style="display:none"></canvas></td>
<td><canvas width="320" height="240" id="remote-view-4-canvas"
style="display:none"></canvas></td>
</tr>
</table>
</body>
</html>