blob: 588f6436161d571a6970f93199cffc2e06e73d4e [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 audio2 = document.querySelector('audio#audio2');
var callButton = document.querySelector('button#callButton');
var hangupButton = document.querySelector('button#hangupButton');
var codecSelector = document.querySelector('select#codec');
hangupButton.disabled = true;
callButton.onclick = call;
hangupButton.onclick = hangup;
var pc1;
var pc2;
var localStream;
var bitrateGraph;
var bitrateSeries;
var packetGraph;
var packetSeries;
var lastResult;
var offerOptions = {
offerToReceiveAudio: 1,
offerToReceiveVideo: 0,
voiceActivityDetection: false
};
function gotStream(stream) {
hangupButton.disabled = false;
trace('Received local stream');
localStream = stream;
var audioTracks = localStream.getAudioTracks();
if (audioTracks.length > 0) {
trace('Using Audio device: ' + audioTracks[0].label);
}
localStream.getTracks().forEach(
function(track) {
pc1.addTrack(
track,
localStream
);
}
);
trace('Adding Local Stream to peer connection');
pc1.createOffer(
offerOptions
).then(
gotDescription1,
onCreateSessionDescriptionError
);
bitrateSeries = new TimelineDataSeries();
bitrateGraph = new TimelineGraphView('bitrateGraph', 'bitrateCanvas');
bitrateGraph.updateEndDate();
packetSeries = new TimelineDataSeries();
packetGraph = new TimelineGraphView('packetGraph', 'packetCanvas');
packetGraph.updateEndDate();
}
function onCreateSessionDescriptionError(error) {
trace('Failed to create session description: ' + error.toString());
}
function call() {
callButton.disabled = true;
codecSelector.disabled = true;
trace('Starting call');
var servers = null;
var pcConstraints = {
'optional': []
};
pc1 = new RTCPeerConnection(servers, pcConstraints);
trace('Created local peer connection object pc1');
pc1.onicecandidate = function(e) {
onIceCandidate(pc1, e);
};
pc2 = new RTCPeerConnection(servers, pcConstraints);
trace('Created remote peer connection object pc2');
pc2.onicecandidate = function(e) {
onIceCandidate(pc2, e);
};
pc2.ontrack = gotRemoteStream;
trace('Requesting local stream');
navigator.mediaDevices.getUserMedia({
audio: true,
video: false
})
.then(gotStream)
.catch(function(e) {
alert('getUserMedia() error: ' + e.name);
});
}
function gotDescription1(desc) {
trace('Offer from pc1 \n' + desc.sdp);
pc1.setLocalDescription(desc).then(
function() {
desc.sdp = forceChosenAudioCodec(desc.sdp);
pc2.setRemoteDescription(desc).then(
function() {
pc2.createAnswer().then(
gotDescription2,
onCreateSessionDescriptionError
);
},
onSetSessionDescriptionError
);
},
onSetSessionDescriptionError
);
}
function gotDescription2(desc) {
trace('Answer from pc2 \n' + desc.sdp);
pc2.setLocalDescription(desc).then(
function() {
desc.sdp = forceChosenAudioCodec(desc.sdp);
pc1.setRemoteDescription(desc).then(
function() {
},
onSetSessionDescriptionError
);
},
onSetSessionDescriptionError
);
}
function hangup() {
trace('Ending call');
localStream.getTracks().forEach(function(track) {
track.stop();
});
pc1.close();
pc2.close();
pc1 = null;
pc2 = null;
hangupButton.disabled = true;
callButton.disabled = false;
codecSelector.disabled = false;
}
function gotRemoteStream(e) {
if (audio2.srcObject !== e.streams[0]) {
audio2.srcObject = e.streams[0];
trace('Received remote stream');
}
}
function getOtherPc(pc) {
return (pc === pc1) ? pc2 : pc1;
}
function getName(pc) {
return (pc === pc1) ? 'pc1' : 'pc2';
}
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() {
trace('AddIceCandidate success.');
}
function onAddIceCandidateError(error) {
trace('Failed to add ICE Candidate: ' + error.toString());
}
function onSetSessionDescriptionError(error) {
trace('Failed to set session description: ' + error.toString());
}
function forceChosenAudioCodec(sdp) {
return maybePreferCodec(sdp, 'audio', 'send', codecSelector.value);
}
// Copied from AppRTC's sdputils.js:
// Sets |codec| as the default |type| codec if it's present.
// The format of |codec| is 'NAME/RATE', e.g. 'opus/48000'.
function maybePreferCodec(sdp, type, dir, codec) {
var str = type + ' ' + dir + ' codec';
if (codec === '') {
trace('No preference on ' + str + '.');
return sdp;
}
trace('Prefer ' + str + ': ' + codec);
var sdpLines = sdp.split('\r\n');
// Search for m line.
var mLineIndex = findLine(sdpLines, 'm=', type);
if (mLineIndex === null) {
return sdp;
}
// If the codec is available, set it as the default in m line.
var codecIndex = findLine(sdpLines, 'a=rtpmap', codec);
console.log('codecIndex', codecIndex);
if (codecIndex) {
var payload = getCodecPayloadType(sdpLines[codecIndex]);
if (payload) {
sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], payload);
}
}
sdp = sdpLines.join('\r\n');
return sdp;
}
// Find the line in sdpLines that starts with |prefix|, and, if specified,
// contains |substr| (case-insensitive search).
function findLine(sdpLines, prefix, substr) {
return findLineInRange(sdpLines, 0, -1, prefix, substr);
}
// Find the line in sdpLines[startLine...endLine - 1] that starts with |prefix|
// and, if specified, contains |substr| (case-insensitive search).
function findLineInRange(sdpLines, startLine, endLine, prefix, substr) {
var realEndLine = endLine !== -1 ? endLine : sdpLines.length;
for (var i = startLine; i < realEndLine; ++i) {
if (sdpLines[i].indexOf(prefix) === 0) {
if (!substr ||
sdpLines[i].toLowerCase().indexOf(substr.toLowerCase()) !== -1) {
return i;
}
}
}
return null;
}
// Gets the codec payload type from an a=rtpmap:X line.
function getCodecPayloadType(sdpLine) {
var pattern = new RegExp('a=rtpmap:(\\d+) \\w+\\/\\d+');
var result = sdpLine.match(pattern);
return (result && result.length === 2) ? result[1] : null;
}
// Returns a new m= line with the specified codec as the first one.
function setDefaultCodec(mLine, payload) {
var elements = mLine.split(' ');
// Just copy the first three parameters; codec order starts on fourth.
var newLine = elements.slice(0, 3);
// Put target payload first and copy in the rest.
newLine.push(payload);
for (var i = 3; i < elements.length; i++) {
if (elements[i] !== payload) {
newLine.push(elements[i]);
}
}
return newLine.join(' ');
}
// query getStats every second
window.setInterval(function() {
if (!window.pc1) {
return;
}
window.pc1.getStats(null).then(function(res) {
res.forEach(function(report) {
var bytes;
var packets;
var now = report.timestamp;
if ((report.type === 'outboundrtp') ||
(report.type === 'outbound-rtp') ||
(report.type === 'ssrc' && report.bytesSent)) {
bytes = report.bytesSent;
packets = report.packetsSent;
if (lastResult && lastResult.get(report.id)) {
// calculate bitrate
var bitrate = 8 * (bytes - lastResult.get(report.id).bytesSent) /
(now - lastResult.get(report.id).timestamp);
// append to chart
bitrateSeries.addPoint(now, bitrate);
bitrateGraph.setDataSeries([bitrateSeries]);
bitrateGraph.updateEndDate();
// calculate number of packets and append to chart
packetSeries.addPoint(now, packets -
lastResult.get(report.id).packetsSent);
packetGraph.setDataSeries([packetSeries]);
packetGraph.updateEndDate();
}
}
});
lastResult = res;
});
}, 1000);