blob: 323c207f5e63e0ca77458e4254e12072c9ac9e2d [file] [log] [blame]
<!doctype html>
<meta charset=utf-8>
<!-- This file contains a test that waits for 2 seconds. -->
<meta name="timeout" content="long">
<title>captureTimestamp attribute in RTCRtpSynchronizationSource</title>
<div><video id="remote" width="124" height="124" autoplay></video></div>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/webrtc/RTCPeerConnection-helper.js"></script>
<script src="/webrtc/RTCStats-helper.js"></script>
<script>
'use strict';
var kAbsCaptureTime =
'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time';
function addHeaderExtensionToSdp(sdp, uri) {
const extmap = new RegExp('a=extmap:(\\d+)');
let sdpLines = sdp.split('\r\n');
// This assumes at most one audio m= section and one video m= section.
// If more are present, only the first section of each kind is munged.
for (const section of ['audio', 'video']) {
let found_section = false;
let maxId = undefined;
let maxIdLine = undefined;
let extmapAllowMixed = false;
// find the largest header extension id for section.
for (let i = 0; i < sdpLines.length; ++i) {
if (!found_section) {
if (sdpLines[i].startsWith('m=' + section)) {
found_section = true;
}
continue;
} else {
if (sdpLines[i].startsWith('m=')) {
// end of section
break;
}
}
if (sdpLines[i] === 'a=extmap-allow-mixed') {
extmapAllowMixed = true;
}
let result = sdpLines[i].match(extmap);
if (result && result.length === 2) {
if (maxId == undefined || result[1] > maxId) {
maxId = parseInt(result[1]);
maxIdLine = i;
}
}
}
if (maxId == 14 && !extmapAllowMixed) {
// Reaching the limit of one byte header extension. Adding two byte header
// extension support.
sdpLines.splice(maxIdLine + 1, 0, 'a=extmap-allow-mixed');
}
if (maxIdLine !== undefined) {
sdpLines.splice(maxIdLine + 1, 0,
'a=extmap:' + (maxId + 1).toString() + ' ' + uri);
}
}
return sdpLines.join('\r\n');
}
// TODO(crbug.com/1051821): Use RTP header extension API instead of munging
// when the RTP header extension API is implemented.
async function addAbsCaptureTimeAndExchangeOffer(caller, callee) {
let offer = await caller.createOffer();
// Absolute capture time header extension may not be offered by default,
// in such case, munge the SDP.
offer.sdp = addHeaderExtensionToSdp(offer.sdp, kAbsCaptureTime);
await caller.setLocalDescription(offer);
return callee.setRemoteDescription(offer);
}
// TODO(crbug.com/1051821): Use RTP header extension API instead of munging
// when the RTP header extension API is implemented.
async function checkAbsCaptureTimeAndExchangeAnswer(caller, callee,
absCaptureTimeAnswered) {
let answer = await callee.createAnswer();
const extmap = new RegExp('a=extmap:\\d+ ' + kAbsCaptureTime + '\r\n', 'g');
if (answer.sdp.match(extmap) == null) {
// We expect that absolute capture time RTP header extension is answered.
// But if not, there is no need to proceed with the test.
assert_false(absCaptureTimeAnswered, 'Absolute capture time RTP ' +
'header extension is not answered');
} else {
if (!absCaptureTimeAnswered) {
// We expect that absolute capture time RTP header extension is not
// answered, but it is, then we munge the answer to remove it.
answer.sdp = answer.sdp.replace(extmap, '');
}
}
await callee.setLocalDescription(answer);
return caller.setRemoteDescription(answer);
}
async function exchangeOfferAndListenToOntrack(t, caller, callee,
absCaptureTimeOffered) {
const ontrackPromise = addEventListenerPromise(t, callee, 'track');
// Absolute capture time header extension is expected not offered by default,
// and thus munging is needed to enable it.
await absCaptureTimeOffered
? addAbsCaptureTimeAndExchangeOffer(caller, callee)
: exchangeOffer(caller, callee);
return ontrackPromise;
}
async function initiateSingleTrackCall(t, cap, absCaptureTimeOffered,
absCaptureTimeAnswered) {
const caller = new RTCPeerConnection();
t.add_cleanup(() => caller.close());
const callee = new RTCPeerConnection();
t.add_cleanup(() => callee.close());
const stream = await getNoiseStream(cap);
stream.getTracks().forEach(track => {
caller.addTrack(track, stream);
t.add_cleanup(() => track.stop());
});
// TODO(crbug.com/988432): `getSynchronizationSources() on the audio side
// needs a hardware sink for the returned dictionary entries to get updated.
const remoteVideo = document.getElementById('remote');
callee.ontrack = e => {
remoteVideo.srcObject = e.streams[0];
}
exchangeIceCandidates(caller, callee);
await exchangeOfferAndListenToOntrack(t, caller, callee,
absCaptureTimeOffered);
// Exchange answer and check whether the absolute capture time RTP header
// extension is answered.
await checkAbsCaptureTimeAndExchangeAnswer(caller, callee,
absCaptureTimeAnswered);
return [caller, callee];
}
function listenForCaptureTimestamp(t, receiver) {
return new Promise((resolve) => {
function listen() {
const ssrcs = receiver.getSynchronizationSources();
assert_true(ssrcs != undefined);
if (ssrcs.length > 0) {
assert_equals(ssrcs.length, 1);
if (ssrcs[0].captureTimestamp != undefined) {
resolve(ssrcs[0].captureTimestamp);
return;
}
}
t.step_timeout(listen, 0);
};
listen();
});
}
// This test only passes if the implementation is sending the absolute capture
// timestamp header extension.
for (const kind of ['audio', 'video']) {
promise_test(async t => {
const [caller, callee] = await initiateSingleTrackCall(
t, {[kind]: true}, false, false);
const receiver = callee.getReceivers()[0];
for (const ssrc of await listenForSSRCs(t, receiver)) {
assert_equals(typeof ssrc.captureTimestamp, 'undefined');
}
}, '[' + kind + '] getSynchronizationSources() should not contain ' +
'captureTimestamp if absolute capture time RTP header extension is not ' +
'offered');
promise_test(async t => {
const [caller, callee] = await initiateSingleTrackCall(
t, {[kind]: true}, false, false);
const receiver = callee.getReceivers()[0];
for (const ssrc of await listenForSSRCs(t, receiver)) {
assert_equals(typeof ssrc.captureTimestamp, 'undefined');
}
}, '[' + kind + '] getSynchronizationSources() should not contain ' +
'captureTimestamp if absolute capture time RTP header extension is ' +
'offered, but not answered');
promise_test(async t => {
const [caller, callee] = await initiateSingleTrackCall(
t, {[kind]: true}, true, true);
const receiver = callee.getReceivers()[0];
await listenForCaptureTimestamp(t, receiver);
}, '[' + kind + '] getSynchronizationSources() should contain ' +
'captureTimestamp if absolute capture time RTP header extension is ' +
'negotiated');
}
promise_test(async t => {
const [caller, callee] = await initiateSingleTrackCall(
t, {audio: true, video: true}, true, true);
const receivers = callee.getReceivers();
assert_equals(receivers.length, 2);
let captureTimestamps = [undefined, undefined];
const t0 = performance.now();
for (let i = 0; i < 2; ++i) {
captureTimestamps[i] = await listenForCaptureTimestamp(t, receivers[i]);
}
const t1 = performance.now();
assert_less_than(Math.abs(captureTimestamps[0] - captureTimestamps[1]),
t1 - t0);
}, 'Audio and video RTCRtpSynchronizationSource.captureTimestamp are ' +
'comparable');
</script>