| Minyue Li | 440ec81 | 2020-03-29 01:52:36 | [diff] [blame] | 1 | <!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 |  | 
|  | 14 | var kAbsCaptureTime = | 
|  | 15 | 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time'; | 
|  | 16 |  | 
|  | 17 | function 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. | 
|  | 70 | async 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. | 
|  | 83 | async 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 McGruer | ae7042f | 2020-04-29 15:28:43 | [diff] [blame] | 91 | assert_false(absCaptureTimeAnswered, 'Absolute capture time RTP ' + | 
| Minyue Li | 440ec81 | 2020-03-29 01:52:36 | [diff] [blame] | 92 | '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 |  | 
|  | 105 | async 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 |  | 
|  | 116 | async 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 |  | 
|  | 150 | function 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. | 
|  | 170 | for (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 |  | 
|  | 205 | promise_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> |