blob: 323c207f5e63e0ca77458e4254e12072c9ac9e2d [file] [log] [blame]
Minyue Li440ec812020-03-29 01:52:361<!doctype html>
2<meta charset=utf-8>
3<!-- This file contains a test that waits for 2 seconds. -->
4<meta name="timeout" content="long">
5<title>captureTimestamp attribute in RTCRtpSynchronizationSource</title>
6<div><video id="remote" width="124" height="124" autoplay></video></div>
7<script src="/resources/testharness.js"></script>
8<script src="/resources/testharnessreport.js"></script>
9<script src="/webrtc/RTCPeerConnection-helper.js"></script>
10<script src="/webrtc/RTCStats-helper.js"></script>
11<script>
12'use strict';
13
14var kAbsCaptureTime =
15 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time';
16
17function addHeaderExtensionToSdp(sdp, uri) {
18 const extmap = new RegExp('a=extmap:(\\d+)');
19 let sdpLines = sdp.split('\r\n');
20
21 // This assumes at most one audio m= section and one video m= section.
22 // If more are present, only the first section of each kind is munged.
23 for (const section of ['audio', 'video']) {
24 let found_section = false;
25 let maxId = undefined;
26 let maxIdLine = undefined;
27 let extmapAllowMixed = false;
28
29 // find the largest header extension id for section.
30 for (let i = 0; i < sdpLines.length; ++i) {
31 if (!found_section) {
32 if (sdpLines[i].startsWith('m=' + section)) {
33 found_section = true;
34 }
35 continue;
36 } else {
37 if (sdpLines[i].startsWith('m=')) {
38 // end of section
39 break;
40 }
41 }
42
43 if (sdpLines[i] === 'a=extmap-allow-mixed') {
44 extmapAllowMixed = true;
45 }
46 let result = sdpLines[i].match(extmap);
47 if (result && result.length === 2) {
48 if (maxId == undefined || result[1] > maxId) {
49 maxId = parseInt(result[1]);
50 maxIdLine = i;
51 }
52 }
53 }
54
55 if (maxId == 14 && !extmapAllowMixed) {
56 // Reaching the limit of one byte header extension. Adding two byte header
57 // extension support.
58 sdpLines.splice(maxIdLine + 1, 0, 'a=extmap-allow-mixed');
59 }
60 if (maxIdLine !== undefined) {
61 sdpLines.splice(maxIdLine + 1, 0,
62 'a=extmap:' + (maxId + 1).toString() + ' ' + uri);
63 }
64 }
65 return sdpLines.join('\r\n');
66}
67
68// TODO(crbug.com/1051821): Use RTP header extension API instead of munging
69// when the RTP header extension API is implemented.
70async function addAbsCaptureTimeAndExchangeOffer(caller, callee) {
71 let offer = await caller.createOffer();
72
73 // Absolute capture time header extension may not be offered by default,
74 // in such case, munge the SDP.
75 offer.sdp = addHeaderExtensionToSdp(offer.sdp, kAbsCaptureTime);
76
77 await caller.setLocalDescription(offer);
78 return callee.setRemoteDescription(offer);
79}
80
81// TODO(crbug.com/1051821): Use RTP header extension API instead of munging
82// when the RTP header extension API is implemented.
83async function checkAbsCaptureTimeAndExchangeAnswer(caller, callee,
84 absCaptureTimeAnswered) {
85 let answer = await callee.createAnswer();
86
87 const extmap = new RegExp('a=extmap:\\d+ ' + kAbsCaptureTime + '\r\n', 'g');
88 if (answer.sdp.match(extmap) == null) {
89 // We expect that absolute capture time RTP header extension is answered.
90 // But if not, there is no need to proceed with the test.
Stephen McGruerae7042f2020-04-29 15:28:4391 assert_false(absCaptureTimeAnswered, 'Absolute capture time RTP ' +
Minyue Li440ec812020-03-29 01:52:3692 'header extension is not answered');
93 } else {
94 if (!absCaptureTimeAnswered) {
95 // We expect that absolute capture time RTP header extension is not
96 // answered, but it is, then we munge the answer to remove it.
97 answer.sdp = answer.sdp.replace(extmap, '');
98 }
99 }
100
101 await callee.setLocalDescription(answer);
102 return caller.setRemoteDescription(answer);
103}
104
105async function exchangeOfferAndListenToOntrack(t, caller, callee,
106 absCaptureTimeOffered) {
107 const ontrackPromise = addEventListenerPromise(t, callee, 'track');
108 // Absolute capture time header extension is expected not offered by default,
109 // and thus munging is needed to enable it.
110 await absCaptureTimeOffered
111 ? addAbsCaptureTimeAndExchangeOffer(caller, callee)
112 : exchangeOffer(caller, callee);
113 return ontrackPromise;
114}
115
116async function initiateSingleTrackCall(t, cap, absCaptureTimeOffered,
117 absCaptureTimeAnswered) {
118 const caller = new RTCPeerConnection();
119 t.add_cleanup(() => caller.close());
120 const callee = new RTCPeerConnection();
121 t.add_cleanup(() => callee.close());
122
123 const stream = await getNoiseStream(cap);
124 stream.getTracks().forEach(track => {
125 caller.addTrack(track, stream);
126 t.add_cleanup(() => track.stop());
127 });
128
129 // TODO(crbug.com/988432): `getSynchronizationSources() on the audio side
130 // needs a hardware sink for the returned dictionary entries to get updated.
131 const remoteVideo = document.getElementById('remote');
132
133 callee.ontrack = e => {
134 remoteVideo.srcObject = e.streams[0];
135 }
136
137 exchangeIceCandidates(caller, callee);
138
139 await exchangeOfferAndListenToOntrack(t, caller, callee,
140 absCaptureTimeOffered);
141
142 // Exchange answer and check whether the absolute capture time RTP header
143 // extension is answered.
144 await checkAbsCaptureTimeAndExchangeAnswer(caller, callee,
145 absCaptureTimeAnswered);
146
147 return [caller, callee];
148}
149
150function listenForCaptureTimestamp(t, receiver) {
151 return new Promise((resolve) => {
152 function listen() {
153 const ssrcs = receiver.getSynchronizationSources();
154 assert_true(ssrcs != undefined);
155 if (ssrcs.length > 0) {
156 assert_equals(ssrcs.length, 1);
157 if (ssrcs[0].captureTimestamp != undefined) {
158 resolve(ssrcs[0].captureTimestamp);
159 return;
160 }
161 }
162 t.step_timeout(listen, 0);
163 };
164 listen();
165 });
166}
167
168// This test only passes if the implementation is sending the absolute capture
169// timestamp header extension.
170for (const kind of ['audio', 'video']) {
171 promise_test(async t => {
172 const [caller, callee] = await initiateSingleTrackCall(
173 t, {[kind]: true}, false, false);
174 const receiver = callee.getReceivers()[0];
175
176 for (const ssrc of await listenForSSRCs(t, receiver)) {
177 assert_equals(typeof ssrc.captureTimestamp, 'undefined');
178 }
179 }, '[' + kind + '] getSynchronizationSources() should not contain ' +
180 'captureTimestamp if absolute capture time RTP header extension is not ' +
181 'offered');
182
183 promise_test(async t => {
184 const [caller, callee] = await initiateSingleTrackCall(
185 t, {[kind]: true}, false, false);
186 const receiver = callee.getReceivers()[0];
187
188 for (const ssrc of await listenForSSRCs(t, receiver)) {
189 assert_equals(typeof ssrc.captureTimestamp, 'undefined');
190 }
191 }, '[' + kind + '] getSynchronizationSources() should not contain ' +
192 'captureTimestamp if absolute capture time RTP header extension is ' +
193 'offered, but not answered');
194
195 promise_test(async t => {
196 const [caller, callee] = await initiateSingleTrackCall(
197 t, {[kind]: true}, true, true);
198 const receiver = callee.getReceivers()[0];
199 await listenForCaptureTimestamp(t, receiver);
200 }, '[' + kind + '] getSynchronizationSources() should contain ' +
201 'captureTimestamp if absolute capture time RTP header extension is ' +
202 'negotiated');
203}
204
205promise_test(async t => {
206 const [caller, callee] = await initiateSingleTrackCall(
207 t, {audio: true, video: true}, true, true);
208 const receivers = callee.getReceivers();
209 assert_equals(receivers.length, 2);
210
211 let captureTimestamps = [undefined, undefined];
212 const t0 = performance.now();
213 for (let i = 0; i < 2; ++i) {
214 captureTimestamps[i] = await listenForCaptureTimestamp(t, receivers[i]);
215 }
216 const t1 = performance.now();
217 assert_less_than(Math.abs(captureTimestamps[0] - captureTimestamps[1]),
218 t1 - t0);
219}, 'Audio and video RTCRtpSynchronizationSource.captureTimestamp are ' +
220 'comparable');
221
222</script>