blob: 1a1cf917e33a2d7778257c74199b39bf97a26bf6 [file] [log] [blame]
/*
* Copyright 2017 The Chromium Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
'use strict';
var startButton = document.getElementById('startButton');
var callButton = document.getElementById('callButton');
var hangupButton = document.getElementById('hangupButton');
callButton.disabled = true;
hangupButton.disabled = true;
startButton.onclick = start;
callButton.onclick = startTest;
hangupButton.onclick = hangup;
var startTime;
var localVideo = document.getElementById('localVideo');
var remoteVideo = document.getElementById('remoteVideo');
function trace(arg) {
var now = (window.performance.now() / 1000).toFixed(3);
console.log(now + ': ', arg);
}
function getSelectedVideoCodec() {
var codec;
if (document.getElementById('H264').checked) {
codec = document.getElementById('H264').value;
} else if (document.getElementById('VP8').checked) {
codec = document.getElementById('VP8').value;
} else {
codec = document.getElementById('VP9').value;
}
return codec;
}
function Failure(message) {
this.message = message;
this.name = 'Failure';
}
localVideo.addEventListener('loadedmetadata', function() {
trace('Local video videoWidth: ' + this.videoWidth +
'px, videoHeight: ' + this.videoHeight + 'px');
});
remoteVideo.addEventListener('loadedmetadata', function() {
trace('Remote video videoWidth: ' + this.videoWidth +
'px, videoHeight: ' + this.videoHeight + 'px');
});
remoteVideo.onresize = function() {
trace('Remote video size changed to ' +
remoteVideo.videoWidth + 'x' + remoteVideo.videoHeight);
// We'll use the first onsize callback as an indication that video has started
// playing out.
if (startTime) {
var elapsedTime = window.performance.now() - startTime;
trace('Setup time: ' + elapsedTime.toFixed(3) + 'ms');
startTime = null;
}
};
var localStream;
var pc1;
var pc2;
var offerOptions = {
offerToReceiveAudio: 1,
offerToReceiveVideo: 1
};
function getName(pc) {
return (pc === pc1) ? 'pc1' : 'pc2';
}
function getOtherPc(pc) {
return (pc === pc1) ? pc2 : pc1;
}
function gotStream(stream) {
trace('Received local stream');
localVideo.srcObject = stream;
localStream = stream;
callButton.disabled = false;
}
function start() {
trace('Requesting local stream');
startButton.disabled = true;
navigator.mediaDevices.getUserMedia({
audio: true,
video: {
width: {exact: 1280},
height: {exact: 720},
},
})
.then(gotStream)
.catch(function(e) {
alert('getUserMedia() error: ' + e.name);
});
}
function call() {
callButton.disabled = true;
hangupButton.disabled = false;
trace('Starting call');
startTime = window.performance.now();
var videoTracks = localStream.getVideoTracks();
var audioTracks = localStream.getAudioTracks();
if (videoTracks.length > 0) {
trace('Using video device: ' + videoTracks[0].label);
}
if (audioTracks.length > 0) {
trace('Using audio device: ' + audioTracks[0].label);
}
var servers = null;
pc1 = new RTCPeerConnection(servers);
trace('Created local peer connection object pc1');
pc1.onicecandidate = function(e) {
onIceCandidate(pc1, e);
};
pc2 = new RTCPeerConnection(servers);
trace('Created remote peer connection object pc2');
pc2.onicecandidate = function(e) {
onIceCandidate(pc2, e);
};
pc1.oniceconnectionstatechange = function(e) {
onIceStateChange(pc1, e);
};
pc2.oniceconnectionstatechange = function(e) {
onIceStateChange(pc2, e);
};
pc2.ontrack = gotRemoteStream;
localStream.getTracks().forEach(
function(track) {
pc1.addTrack(
track,
localStream
);
}
);
trace('Added local stream to pc1');
trace('pc1 createOffer start');
pc1.createOffer(
offerOptions
).then(
onCreateOfferSuccess,
onCreateSessionDescriptionError
);
}
function onCreateSessionDescriptionError(error) {
trace('Failed to create session description: ' + error.toString());
}
/**
* See |setSdpDefaultCodec|.
*/
function setSdpDefaultVideoCodec(sdp, codec, preferHwCodec) {
return setSdpDefaultCodec(sdp, 'video', codec, preferHwCodec);
}
/**
* Returns a modified version of |sdp| where the |codec| has been promoted to be
* the default codec, i.e. the codec whose ID is first in the list of codecs on
* the 'm=|type|' line, where |type| is 'audio' or 'video'. If |preferHwCodec|
* is true, it will select the last codec with the given name, and if false, it
* will select the first codec with the given name, because HW codecs are listed
* after SW codecs in the SDP list.
* @private
*/
function setSdpDefaultCodec(sdp, type, codec, preferHwCodec) {
var sdpLines = splitSdpLines(sdp);
// Find codec ID, e.g. 100 for 'VP8' if 'a=rtpmap:100 VP8/9000'.
var codecId = findRtpmapId(sdpLines, codec, preferHwCodec);
if (codecId === null) {
throw new Failure('setSdpDefaultCodec',
'Unknown ID for |codec| = \'' + codec + '\'.');
}
// Find 'm=|type|' line, e.g. 'm=video 9 UDP/TLS/RTP/SAVPF 100 101 107 116'.
var mLineNo = findLine(sdpLines, 'm=' + type);
if (mLineNo === null) {
throw new Failure('setSdpDefaultCodec',
'\'m=' + type + '\' line missing from |sdp|.');
}
// Modify video line to use the desired codec as the default.
sdpLines[mLineNo] = setMLineDefaultCodec(sdpLines[mLineNo], codecId);
return mergeSdpLines(sdpLines);
}
/**
* Searches through all |sdpLines| for the 'a=rtpmap:' line for the codec of
* the specified name, returning its ID as an int if found, or null otherwise.
* |codec| is the case-sensitive name of the codec. If |lastInstance|
* is true, it will return the last such ID, and if false, it will return the
* first such ID.
* For example, if |sdpLines| contains 'a=rtpmap:100 VP8/9000' and |codec| is
* 'VP8', this function returns 100.
* @private
*/
function findRtpmapId(sdpLines, codec, lastInstance) {
var lineNo = findRtpmapLine(sdpLines, codec, lastInstance);
if (lineNo === null) {
return null;
}
// Parse <id> from 'a=rtpmap:<id> <codec>/<rate>'.
var id = sdpLines[lineNo].substring(9, sdpLines[lineNo].indexOf(' '));
return parseInt(id);
}
/**
* Finds a 'a=rtpmap:' line from |sdpLines| that contains |contains| and returns
* its line index, or null if no such line was found. |contains| may be the
* codec ID, codec name or bitrate. If |lastInstance| is true, it will return
* the last such line index, and if false, it will return the first such line
* index.
* An 'a=rtpmap:' line looks like this: 'a=rtpmap:<id> <codec>/<rate>'.
*/
function findRtpmapLine(sdpLines, contains, lastInstance) {
if (lastInstance === true) {
for (var i = sdpLines.length - 1; i >= 0 ; i--) {
if (isRtpmapLine(sdpLines[i], contains)) {
return i;
}
}
} else {
for (i = 0; i < sdpLines.length; i++) {
if (isRtpmapLine(sdpLines[i], contains)) {
return i;
}
}
}
return null;
}
/**
* Returns true if |sdpLine| contains |contains| and is of pattern
* 'a=rtpmap:<id> <codec>/<rate>'.
*/
function isRtpmapLine(sdpLine, contains) {
// Is 'a=rtpmap:' line containing |contains| string?
if (sdpLine.startsWith('a=rtpmap:') &&
sdpLine.indexOf(contains) !== -1) {
// Expecting pattern 'a=rtpmap:<id> <codec>/<rate>'.
var pattern = new RegExp('a=rtpmap:(\\d+) \\w+\\/\\d+');
if (!sdpLine.match(pattern)) {
throw new Failure('isRtpmapLine', 'Unexpected "a=rtpmap:" pattern.');
}
return true;
}
return false;
}
/**
* Returns a modified version of |mLine| that has |codecId| first in the list of
* codec IDs. For example, setMLineDefaultCodec(
* 'm=video 9 UDP/TLS/RTP/SAVPF 100 101 107 116 117 96', 107)
* Returns:
* 'm=video 9 UDP/TLS/RTP/SAVPF 107 100 101 116 117 96'
* @private
*/
function setMLineDefaultCodec(mLine, codecId) {
var elements = mLine.split(' ');
// Copy first three elements, codec order starts on fourth.
var newLine = elements.slice(0, 3);
// Put target |codecId| first and copy the rest.
newLine.push(codecId);
for (var i = 3; i < elements.length; i++) {
if (elements[i] !== codecId) {
newLine.push(elements[i]);
}
}
return newLine.join(' ');
}
/** @private */
function splitSdpLines(sdp) {
return sdp.split('\r\n');
}
/** @private */
function mergeSdpLines(sdpLines) {
return sdpLines.join('\r\n');
}
/** @private */
function findLine(lines, lineStartsWith, startingLine = 0) {
for (var i = startingLine; i < lines.length; i++) {
if (lines[i].startsWith(lineStartsWith)) {
return i;
}
}
return null;
}
function onCreateOfferSuccess(desc) {
var videoCodec = getSelectedVideoCodec();
desc.sdp = setSdpDefaultVideoCodec(desc.sdp, videoCodec, videoCodec);
trace('Offer from pc1\n' + desc.sdp);
trace('Ok-' + JSON.stringify(desc));
trace('pc1 setLocalDescription start');
pc1.setLocalDescription(desc).then(
function() {
onSetLocalSuccess(pc1);
},
onSetSessionDescriptionError
);
trace('pc2 setRemoteDescription start');
pc2.setRemoteDescription(desc).then(
function() {
onSetRemoteSuccess(pc2);
},
onSetSessionDescriptionError
);
trace('pc2 createAnswer start');
// Since the 'remote' side has no media stream we need
// to pass in the right constraints in order for it to
// accept the incoming offer of audio and video.
pc2.createAnswer().then(
onCreateAnswerSuccess,
onCreateSessionDescriptionError
);
}
function onSetLocalSuccess(pc) {
trace(getName(pc) + ' setLocalDescription complete');
}
function onSetRemoteSuccess(pc) {
trace(getName(pc) + ' setRemoteDescription complete');
}
function onSetSessionDescriptionError(error) {
trace('Failed to set session description: ' + error.toString());
}
function startTest() {
call();
setInterval(() => {
pc1.getStats((response) => {
trace(response);
});
}, 10 * 1000);
}
function gotRemoteStream(e) {
if (remoteVideo.srcObject !== e.streams[0]) {
remoteVideo.srcObject = e.streams[0];
trace('pc2 received remote stream');
}
}
function onCreateAnswerSuccess(desc) {
trace('Answer from pc2:\n' + desc.sdp);
trace('pc2 setLocalDescription start');
pc2.setLocalDescription(desc).then(
function() {
onSetLocalSuccess(pc2);
},
onSetSessionDescriptionError
);
trace('pc1 setRemoteDescription start');
pc1.setRemoteDescription(desc).then(
function() {
onSetRemoteSuccess(pc1);
},
onSetSessionDescriptionError
);
}
function onIceCandidate(pc, event) {
getOtherPc(pc).addIceCandidate(event.candidate)
.then(
function() {
onAddIceCandidateSuccess(pc);
},
function(err) {
onAddIceCandidateError(pc, err);
}
);
trace(getName(pc) + ' ICE candidate: \n' + (event.candidate ?
event.candidate.candidate : '(null)'));
}
function onAddIceCandidateSuccess(pc) {
trace(getName(pc) + ' addIceCandidate success');
}
function onAddIceCandidateError(pc, error) {
trace(getName(pc) + ' failed to add ICE Candidate: ' + error.toString());
}
function onIceStateChange(pc, event) {
if (pc) {
trace(getName(pc) + ' ICE state: ' + pc.iceConnectionState);
console.log('ICE state change event: ', event);
}
}
function hangup() {
trace('Ending call');
pc1.close();
pc2.close();
pc1 = null;
pc2 = null;
hangupButton.disabled = true;
callButton.disabled = false;
}