blob: e3f822bebdcc1ea4d62d2f01cbd6b1e5dac9eb92 [file] [log] [blame]
/**
* Copyright 2014 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.
*/
/**
* The one and only peer connection in this page.
* @private
*/
var gPeerConnection = null;
/**
* This stores ICE candidates generated on this side.
* @private
*/
var gIceCandidates = [];
/**
* Keeps track of whether we have seen crypto information in the SDP.
* @private
*/
var gHasSeenCryptoInSdp = 'no-crypto-seen';
/**
* The default video codec that should be used.
* @private
*/
var gDefaultVideoCodec = null;
/**
* Flag to indicate if Opus Dtx should be enabled.
* @private
*/
var gOpusDtx = false;
// Public interface to tests. These are expected to be called with
// ExecuteJavascript invocations from the browser tests and will return answers
// through the DOM automation controller.
/**
* Creates a peer connection. Must be called before most other public functions
* in this file. Alternatively, see |preparePeerConnectionWithCertificate|.
* @param {Object} keygenAlgorithm Unless null, this is an |AlgorithmIdentifier|
* to be used as parameter to |RTCPeerConnection.generateCertificate|. The
* resulting certificate will be used by the peer connection. If null, a default
* certificate is generated by the |RTCPeerConnection| instead.
*/
function preparePeerConnection(keygenAlgorithm = null) {
if (gPeerConnection !== null)
throw failTest('Creating peer connection, but we already have one.');
if (keygenAlgorithm === null) {
gPeerConnection = createPeerConnection_(null);
returnToTest('ok-peerconnection-created');
} else {
RTCPeerConnection.generateCertificate(keygenAlgorithm).then(
function(certificate) {
preparePeerConnectionWithCertificate(certificate);
},
function() {
failTest('Certificate generation failed. keygenAlgorithm: ' +
JSON.stringify(keygenAlgorithm));
});
}
}
/**
* Creates a peer connection. Must be called before most other public functions
* in this file. Alternatively, see |preparePeerConnection|.
* @param {!Object} certificate The |RTCCertificate| that will be used by the
* peer connection.
*/
function preparePeerConnectionWithCertificate(certificate) {
if (gPeerConnection !== null)
throw failTest('Creating peer connection, but we already have one.');
gPeerConnection = createPeerConnection_(
{iceServers:[], certificates:[certificate]});
returnToTest('ok-peerconnection-created');
}
/**
* Sets the flag to force Opus Dtx to be used when creating an offer.
*/
function forceOpusDtx() {
gOpusDtx = true;
returnToTest('ok-forced');
}
/**
* Sets the default video codec be used when creating an offer.
* @param {string} videoCodec promotes the specified codec to be the default
* video codec, e.g. the first one in the list on the 'm=video' SDP offer
* line. |videoCodec| is the case-sensitive codec name, e.g. 'VP8' or
* 'H264'.
*/
function forceVideoCodec(videoCodec) {
gDefaultVideoCodec = videoCodec;
returnToTest('ok-forced');
}
/**
* Asks this page to create a local offer.
*
* Returns a string on the format ok-(JSON encoded session description).
*
* @param {!Object} constraints Any createOffer constraints.
*/
function createLocalOffer(constraints) {
peerConnection_().createOffer(
function(localOffer) {
success('createOffer');
setLocalDescription(peerConnection, localOffer);
if (gDefaultVideoCodec !== null) {
localOffer.sdp = setSdpDefaultVideoCodec(localOffer.sdp,
gDefaultVideoCodec);
}
if (gOpusDtx) {
localOffer.sdp = setOpusDtxEnabled(localOffer.sdp);
}
returnToTest('ok-' + JSON.stringify(localOffer));
},
function(error) { failure('createOffer', error); },
constraints);
}
/**
* Asks this page to accept an offer and generate an answer.
*
* Returns a string on the format ok-(JSON encoded session description).
*
* @param {!string} sessionDescJson A JSON-encoded session description of type
* 'offer'.
* @param {!Object} constraints Any createAnswer constraints.
*/
function receiveOfferFromPeer(sessionDescJson, constraints) {
offer = parseJson_(sessionDescJson);
if (!offer.type)
failTest('Got invalid session description from peer: ' + sessionDescJson);
if (offer.type != 'offer')
failTest('Expected to receive offer from peer, got ' + offer.type);
var sessionDescription = new RTCSessionDescription(offer);
peerConnection_().setRemoteDescription(
sessionDescription,
function() { success('setRemoteDescription'); },
function(error) { failure('setRemoteDescription', error); });
peerConnection_().createAnswer(
function(answer) {
success('createAnswer');
setLocalDescription(peerConnection, answer);
if (gOpusDtx) {
answer.sdp = setOpusDtxEnabled(answer.sdp);
}
returnToTest('ok-' + JSON.stringify(answer));
},
function(error) { failure('createAnswer', error); },
constraints);
}
/**
* Verifies that the codec previously set using forceVideoCodec() is the
* default video codec, e.g. the first one in the list on the 'm=video' SDP
* answer line. If this is not the case, |failure| occurs. If no codec was
* previously set using forceVideoCodec(), this function will return
* 'ok-no-default-set'.
*
* @param {!string} sessionDescJson A JSON-encoded session description.
*/
function verifyDefaultVideoCodec(sessionDescJson) {
var sessionDesc = parseJson_(sessionDescJson);
if (gDefaultVideoCodec === null) {
returnToTest('ok-no-default-set');
return;
}
if (!sessionDesc.type) {
failure('verifyDefaultVideoCodec',
'Invalid session description: ' + sessionDescJson);
}
var defaultVideoCodec = getSdpDefaultVideoCodec(sessionDesc.sdp);
if (defaultVideoCodec === null) {
failure('verifyDefaultVideoCodec',
'Could not determine default video codec.');
}
if (gDefaultVideoCodec !== defaultVideoCodec) {
failure('verifyDefaultVideoCodec',
'Expected default video codec ' + gDefaultVideoCodec +
', got ' + defaultVideoCodec + '.');
}
returnToTest('ok-verified');
}
/**
* Asks this page to accept an answer generated by the peer in response to a
* previous offer by this page
*
* Returns a string ok-accepted-answer on success.
*
* @param {!string} sessionDescJson A JSON-encoded session description of type
* 'answer'.
*/
function receiveAnswerFromPeer(sessionDescJson) {
answer = parseJson_(sessionDescJson);
if (!answer.type)
failTest('Got invalid session description from peer: ' + sessionDescJson);
if (answer.type != 'answer')
failTest('Expected to receive answer from peer, got ' + answer.type);
var sessionDescription = new RTCSessionDescription(answer);
peerConnection_().setRemoteDescription(
sessionDescription,
function() {
success('setRemoteDescription');
returnToTest('ok-accepted-answer');
},
function(error) { failure('setRemoteDescription', error); });
}
/**
* Adds the local stream to the peer connection. You will have to re-negotiate
* the call for this to take effect in the call.
*/
function addLocalStream() {
addLocalStreamToPeerConnection(peerConnection_());
returnToTest('ok-added');
}
/**
* Loads a file with WebAudio and connects it to the peer connection.
*
* The loadAudioAndAddToPeerConnection will return ok-added to the test when
* the sound is loaded and added to the peer connection. The sound will start
* playing when you call playAudioFile.
*
* @param url URL pointing to the file to play. You can assume that you can
* serve files from the repository's file system. For instance, to serve a
* file from chrome/test/data/pyauto_private/webrtc/file.wav, pass in a path
* relative to this directory (e.g. ../pyauto_private/webrtc/file.wav).
*/
function addAudioFile(url) {
loadAudioAndAddToPeerConnection(url, peerConnection_());
}
/**
* Must be called after addAudioFile.
*/
function playAudioFile() {
playPreviouslyLoadedAudioFile(peerConnection_());
returnToTest('ok-playing');
}
/**
* Hangs up a started call. Returns ok-call-hung-up on success.
*/
function hangUp() {
peerConnection_().close();
gPeerConnection = null;
returnToTest('ok-call-hung-up');
}
/**
* Retrieves all ICE candidates generated on this side. Must be called after
* ICE candidate generation is triggered (for instance by running a call
* negotiation). This function will wait if necessary if we're not done
* generating ICE candidates on this side.
*
* Returns a JSON-encoded array of RTCIceCandidate instances to the test.
*/
function getAllIceCandidates() {
if (peerConnection_().iceGatheringState != 'complete') {
console.log('Still ICE gathering - waiting...');
setTimeout(getAllIceCandidates, 100);
return;
}
returnToTest(JSON.stringify(gIceCandidates));
}
/**
* Receives ICE candidates from the peer.
*
* Returns ok-received-candidates to the test on success.
*
* @param iceCandidatesJson a JSON-encoded array of RTCIceCandidate instances.
*/
function receiveIceCandidates(iceCandidatesJson) {
var iceCandidates = parseJson_(iceCandidatesJson);
if (!iceCandidates.length)
throw failTest('Received invalid ICE candidate list from peer: ' +
iceCandidatesJson);
iceCandidates.forEach(function(iceCandidate) {
if (!iceCandidate.candidate)
failTest('Received invalid ICE candidate from peer: ' +
iceCandidatesJson);
peerConnection_().addIceCandidate(new RTCIceCandidate(iceCandidate,
function() { success('addIceCandidate'); },
function(error) { failure('addIceCandidate', error); }
));
});
returnToTest('ok-received-candidates');
}
/**
* Sets the mute state of the selected media element.
*
* Returns ok-muted on success.
*
* @param elementId The id of the element to mute.
* @param muted The mute state to set.
*/
function setMediaElementMuted(elementId, muted) {
var element = document.getElementById(elementId);
if (!element)
throw failTest('Cannot mute ' + elementId + '; does not exist.');
element.muted = muted;
returnToTest('ok-muted');
}
/**
* Returns
*/
function hasSeenCryptoInSdp() {
returnToTest(gHasSeenCryptoInSdp);
}
/**
* Verifies that |RTCPeerConnection.getStats| returns stats.
*
* Returns ok-got-stats on success.
*/
function verifyStatsGenerated() {
peerConnection_().getStats(
function(response) {
var reports = response.result();
var numStats = 0;
for (var i = 0; i < reports.length; i++) {
var statNames = reports[i].names();
numStats += statNames.length;
for (var j = 0; j < statNames.length; j++) {
var statValue = reports[i].stat(statNames[j]);
if (typeof statValue != 'string')
throw failTest('A stat was returned that is not a string.');
}
}
if (numStats === 0)
throw failTest('No stats was returned by getStats.');
returnToTest('ok-got-stats');
});
}
// Internals.
/** @private */
function createPeerConnection_(rtcConfig) {
try {
peerConnection = new RTCPeerConnection(rtcConfig, {});
} catch (exception) {
throw failTest('Failed to create peer connection: ' + exception);
}
peerConnection.onaddstream = addStreamCallback_;
peerConnection.onremovestream = removeStreamCallback_;
peerConnection.onicecandidate = iceCallback_;
return peerConnection;
}
/** @private */
function peerConnection_() {
if (gPeerConnection == null)
throw failTest('Trying to use peer connection, but none was created.');
return gPeerConnection;
}
/** @private */
function iceCallback_(event) {
if (event.candidate)
gIceCandidates.push(event.candidate);
}
/** @private */
function setLocalDescription(peerConnection, sessionDescription) {
if (sessionDescription.sdp.search('a=crypto') != -1 ||
sessionDescription.sdp.search('a=fingerprint') != -1)
gHasSeenCryptoInSdp = 'crypto-seen';
peerConnection.setLocalDescription(
sessionDescription,
function() { success('setLocalDescription'); },
function(error) { failure('setLocalDescription', error); });
}
/** @private */
function addStreamCallback_(event) {
debug('Receiving remote stream...');
var videoTag = document.getElementById('remote-view');
attachMediaStream(videoTag, event.stream);
}
/** @private */
function removeStreamCallback_(event) {
debug('Call ended.');
document.getElementById('remote-view').src = '';
}
/**
* Parses JSON-encoded session descriptions and ICE candidates.
* @private
*/
function parseJson_(json) {
// Escape since the \r\n in the SDP tend to get unescaped.
jsonWithEscapedLineBreaks = json.replace(/\r\n/g, '\\r\\n');
try {
return JSON.parse(jsonWithEscapedLineBreaks);
} catch (exception) {
failTest('Failed to parse JSON: ' + jsonWithEscapedLineBreaks + ', got ' +
exception);
}
}