h264-unidirectional-codec-offer.https.html (12857B)
1 <!doctype html> 2 <meta charset=utf-8> 3 <title>RTX codec integrity checks</title> 4 <script src="/resources/testharness.js"></script> 5 <script src="/resources/testharnessreport.js"></script> 6 <script> 7 'use strict'; 8 9 // Test unidirectional codec support. 10 // 11 // These tests ensure setCodecPreferences() and SDP negotiation work with 12 // sendonly and recvonly codecs, i.e. using codec values that exist in 13 // RTCRtpSender.getCapabilities() but not RTCRtpReceiver.getCapabilities() or 14 // vice versa. 15 // 16 // While this is not necessarily unique to H264, these tests use H264 because 17 // there are many popular devices where support is unidirectional. If the 18 // prerequisite capabilities are not available to the test it ends in 19 // [PRECONDITION_FAILED] rather than test failures. 20 21 function containsCodec(codecs, codec) { 22 return codecs.find(c => c.mimeType == codec.mimeType && 23 c.sdpFmtpLine == codec.sdpFmtpLine) != null; 24 } 25 26 function getCodecsWithDirection(mimeType, direction) { 27 const sendCodecs = RTCRtpSender.getCapabilities('video').codecs.filter( 28 c => c.mimeType == mimeType); 29 const recvCodecs = RTCRtpReceiver.getCapabilities('video').codecs.filter( 30 c => c.mimeType == mimeType); 31 const codecs = []; 32 if (direction == 'sendrecv') { 33 for (const sendCodec of sendCodecs) { 34 if (containsCodec(recvCodecs, sendCodec)) { 35 codecs.push(sendCodec); 36 } 37 } 38 } else if (direction == 'sendonly') { 39 for (const sendCodec of sendCodecs) { 40 if (!containsCodec(recvCodecs, sendCodec)) { 41 codecs.push(sendCodec); 42 } 43 } 44 } else if (direction == 'recvonly') { 45 for (const recvCodec of recvCodecs) { 46 if (!containsCodec(sendCodecs, recvCodec)) { 47 codecs.push(recvCodec); 48 } 49 } 50 } 51 return codecs; 52 } 53 54 // Returns an array of { mimeType, payloadType, sdpFmtpLine } entries in codec 55 // preference order from the first m-section of the SDP. 56 function parseCodecsFromSdp(sdp) { 57 const codecs = []; 58 const kMLineRegex = /\r\nm=video \d+ [A-Z\/]+ (?<pts>[\d\s]+)\r\n/; 59 const {groups: {pts}} = kMLineRegex.exec(sdp); 60 for (const pt of pts.split(" ")) { 61 const rtpmapRegex = new RegExp(`\r\na=rtpmap:${pt} (?<name>[^ \/]+)\/`); 62 const {groups: {name}} = rtpmapRegex.exec(sdp); 63 const fmtpRegex = new RegExp(`\r\na=fmtp:${pt} (?<sdpFmtpLine>.*)\r\n`); 64 // Guard against there not being an fmtp line. 65 const {groups: {sdpFmtpLine} = {}} = fmtpRegex.exec(sdp) ?? {}; 66 const codec = { mimeType: `video/${name}`, payloadType: pt, sdpFmtpLine }; 67 codecs.push(codec); 68 } 69 return codecs; 70 } 71 72 function replaceCodecInSdp(sdp, oldCodec, newCodec) { 73 // Replace the payload type in the m=video line. 74 sdp = sdp.replace( 75 new RegExp(`(m=video [ \\dA-Z\/]+)${oldCodec.payloadType}`), 76 `$1${newCodec.payloadType}` 77 ); 78 // Replace the payload type in all rtpmap, fmtp and rtcp-fb lines. 79 sdp = sdp.replaceAll( 80 new RegExp(`(a=(rtpmap|fmtp|rtcp-fb):)${oldCodec.payloadType}`, "g"), 81 `$1${newCodec.payloadType}` 82 ); 83 // Lastly, replace the actual "sdpFmtpLine" string. 84 sdp = sdp.replace(oldCodec.sdpFmtpLine, newCodec.sdpFmtpLine); 85 return sdp; 86 } 87 88 promise_test(async t => { 89 const pc1 = new RTCPeerConnection(); 90 t.add_cleanup(() => pc1.close()); 91 const pc2 = new RTCPeerConnection(); 92 t.add_cleanup(() => pc2.close()); 93 94 // We need at least one recvonly codec to test "offer to receive". 95 const recvOnlyCodecs = getCodecsWithDirection('video/H264', 'recvonly'); 96 // A sendrecv codec is used to ensure `pc2` has something to answer with which 97 // makes modifying the SDP answer easier, but because we cannot send the 98 // recvonly codec we have to fake it with remote SDP munging. 99 const sendRecvCodecs = getCodecsWithDirection('video/H264', 'sendrecv'); 100 // If any of the following optional asserts fail the test ends with 101 // [PRECONDITION_FAILED] as opposed to [FAIL]. 102 assert_implements_optional( 103 recvOnlyCodecs.length > 0, 104 `There are no recvonly H264 codecs available in getCapabilities.`); 105 assert_implements_optional( 106 sendRecvCodecs.length > 0, 107 `There are no sendrecv H264 codecs available in getCapabilities.`); 108 const recvOnlyCodec = recvOnlyCodecs[0]; 109 const sendRecvCodec = sendRecvCodecs[0]; 110 111 const pc1Transceiver = pc1.addTransceiver('video', {direction: 'recvonly'}); 112 pc1Transceiver.setCodecPreferences([recvOnlyCodec, sendRecvCodec]); 113 114 // Offer to receive. 115 await pc1.setLocalDescription(); 116 const offeredCodecs = parseCodecsFromSdp(pc1.localDescription.sdp); 117 assert_equals(offeredCodecs.length, 2, 'Two codecs should be offered'); 118 assert_equals(offeredCodecs[0].mimeType, 'video/H264'); 119 assert_true(offeredCodecs[0].sdpFmtpLine == recvOnlyCodec.sdpFmtpLine, 120 `The first offered codec's sdpFmtpLine is the recvonly one.`); 121 assert_equals(offeredCodecs[1].mimeType, 'video/H264'); 122 assert_true(offeredCodecs[1].sdpFmtpLine == sendRecvCodec.sdpFmtpLine, 123 `The second offered codec's sdpFmtpLine is the sendrecv one.`); 124 await pc2.setRemoteDescription(pc1.localDescription); 125 126 // Answer to send. 127 const pc2Transceiver = pc2.getTransceivers()[0]; 128 pc2Transceiver.direction = 'sendonly'; 129 await pc2.setLocalDescription(); 130 // Verify that because we are not capable of sending the first codec, it has 131 // been removed from the SDP answer. 132 const answeredCodecs = parseCodecsFromSdp(pc2.localDescription.sdp); 133 assert_equals(answeredCodecs.length, 1, 'One codec should be answered'); 134 assert_equals(answeredCodecs[0].mimeType, 'video/H264'); 135 assert_true(answeredCodecs[0].sdpFmtpLine == sendRecvCodec.sdpFmtpLine, 136 `The answered codec's sdpFmtpLine is the sendrecv one.`); 137 // Trick `pc1` into thinking `pc2` can send the codec by modifying the SDP. 138 // Receiving media is not testable but this ensures that the SDP is accepted. 139 const modifiedSdp = replaceCodecInSdp( 140 pc2.localDescription.sdp, answeredCodecs[0], offeredCodecs[0]); 141 await pc1.setRemoteDescription({type: 'answer', sdp: modifiedSdp}); 142 }, `Offer to receive a recvonly H264 codec on a recvonly transceiver`); 143 144 promise_test(async t => { 145 const pc = new RTCPeerConnection(); 146 t.add_cleanup(() => pc.close()); 147 148 const h264RecvOnlyCodecs = getCodecsWithDirection('video/H264', 'recvonly'); 149 const h264SendOnlyCodecs = getCodecsWithDirection('video/H264', 'sendonly'); 150 const vp8SendRecvCodecs = getCodecsWithDirection('video/VP8', 'sendrecv'); 151 // If any of the following optional asserts fail the test ends with 152 // [PRECONDITION_FAILED] as opposed to [FAIL]. 153 assert_implements_optional( 154 h264RecvOnlyCodecs.length > 0, 155 `There are no recvonly H264 codecs available in getCapabilities.`); 156 assert_implements_optional( 157 vp8SendRecvCodecs.length > 0, 158 `There are no sendrecv VP8 codecs available in getCapabilities.`); 159 // Find a recvonly codec with the required level (3.1) for both sending and 160 // receiving, that has a corresponding sendonly codec with the same profile 161 // but a higher level. If there is such a match, we should be able to use the 162 // lower level of the two for sendrecv. 163 const kProfileLevelIdRegex = 164 /profile-level-id=(?<profile_idc>..)(?<profile_iop>..)(?<level_idc>..)/; 165 const kProfileLevelIdReqLevelRegex = /profile-level-id=....1f/; 166 const h264RecvOnlyReqLevelCodecs = h264RecvOnlyCodecs.filter( 167 codec => codec.sdpFmtpLine.match(kProfileLevelIdReqLevelRegex)); 168 const h264RecvOnlyCodec = h264RecvOnlyReqLevelCodecs.find(recv => { 169 const {groups: { 170 profile_idc: recvProfile, 171 profile_iop: recvConstraints, 172 level_idc: recvLevelIdc, 173 } 174 } = kProfileLevelIdRegex.exec(recv.sdpFmtpLine); 175 const recvLevel = parseInt(recvLevelIdc, 16); 176 return h264SendOnlyCodecs.find(send => { 177 const {groups: { 178 profile_idc: sendProfile, 179 profile_iop: sendConstraints, 180 level_idc: sendLevelIdc, 181 } 182 } = kProfileLevelIdRegex.exec(send.sdpFmtpLine); 183 const sendLevel = parseInt(sendLevelIdc, 16); 184 return sendProfile == recvProfile && 185 sendConstraints == recvConstraints && 186 sendLevelIdc > recvLevelIdc; 187 }); 188 }); 189 assert_implements_optional( 190 h264RecvOnlyCodec != undefined, 191 `No recvonly profile-level-id=....1f that matches a higher level ` + 192 `sendonly codec`); 193 const vp8SendRecvCodec = vp8SendRecvCodecs[0]; 194 195 const transceiver = pc.addTransceiver('video', {direction: 'sendrecv'}); 196 transceiver.setCodecPreferences([h264RecvOnlyCodec, vp8SendRecvCodec]); 197 198 await pc.setLocalDescription(); 199 const offeredCodecs = parseCodecsFromSdp(pc.localDescription.sdp); 200 // Even though this H264 codec with its level ID is recvonly, we should still 201 // offer to sendrecv it due to sender capabilities being even greater. 202 assert_equals(offeredCodecs.length, 2, 'Two codecs are offered (H264, VP8).'); 203 assert_equals(offeredCodecs[0].mimeType, 'video/H264', 204 'The first offered codec is H264.'); 205 assert_true(offeredCodecs[0].sdpFmtpLine == h264RecvOnlyCodec.sdpFmtpLine, 206 'The offered H264 profile-level-id should match the recvonly ' + 207 'codec since we expect the sender capability to be even higher.'); 208 assert_equals(offeredCodecs[1].mimeType, 'video/VP8', 209 'The second offered codec is VP8.'); 210 }, `Offering a recvonly codec on a sendrecv transceiver`); 211 212 promise_test(async t => { 213 const pc1 = new RTCPeerConnection(); 214 t.add_cleanup(() => pc1.close()); 215 const pc2 = new RTCPeerConnection(); 216 t.add_cleanup(() => pc2.close()); 217 218 // We need at least one sendonly codec to test "offer to send". 219 const sendOnlyCodecs = getCodecsWithDirection('video/H264', 'sendonly'); 220 // A sendrecv codec is used to ensure `pc2` has something to answer with which 221 // makes modifying the SDP answer easier, but because we cannot receive the 222 // sendonly codec we have to fake it with remote SDP munging. 223 const sendRecvCodecs = getCodecsWithDirection('video/H264', 'sendrecv'); 224 // If any of the following optional asserts fail the test ends with 225 // [PRECONDITION_FAILED] as opposed to [FAIL]. 226 assert_implements_optional( 227 sendOnlyCodecs.length > 0, 228 `There are no sendonly H264 codecs available in getCapabilities.`); 229 assert_implements_optional( 230 sendRecvCodecs.length > 0, 231 `There are no sendrecv H264 codecs available in getCapabilities.`); 232 const sendOnlyCodec = sendOnlyCodecs[0]; 233 const sendRecvCodec = sendRecvCodecs[0]; 234 235 const transceiver = pc1.addTransceiver('video', {direction: 'sendonly'}); 236 transceiver.setCodecPreferences([sendOnlyCodec, sendRecvCodec]); 237 238 // Offer to send. 239 await pc1.setLocalDescription(); 240 const offeredCodecs = parseCodecsFromSdp(pc1.localDescription.sdp); 241 assert_equals(offeredCodecs.length, 2, 'Two codecs should be offered'); 242 assert_equals(offeredCodecs[0].mimeType, 'video/H264'); 243 assert_true(offeredCodecs[0].sdpFmtpLine == sendOnlyCodec.sdpFmtpLine, 244 `The first offered codec's sdpFmtpLine is the sendonly one.`); 245 assert_equals(offeredCodecs[1].mimeType, 'video/H264'); 246 assert_true(offeredCodecs[1].sdpFmtpLine == sendRecvCodec.sdpFmtpLine, 247 `The second offered codec's sdpFmtpLine is the sendrecv one.`); 248 await pc2.setRemoteDescription(pc1.localDescription); 249 250 // Answer to receive. 251 await pc2.setLocalDescription(); 252 // Verify that because we are not capable of receiving the first codec, it has 253 // been removed from the SDP answer. 254 const answeredCodecs = parseCodecsFromSdp(pc2.localDescription.sdp); 255 assert_equals(answeredCodecs.length, 1, 'One codec should be answered'); 256 assert_equals(answeredCodecs[0].mimeType, 'video/H264'); 257 assert_true(answeredCodecs[0].sdpFmtpLine == sendRecvCodec.sdpFmtpLine, 258 `The answered codec's sdpFmtpLine is the sendrecv one.`); 259 // Trick `pc1` into thinking `pc2` can receive the codec by modifying the SDP. 260 const modifiedSdp = replaceCodecInSdp( 261 pc2.localDescription.sdp, answeredCodecs[0], offeredCodecs[0]); 262 await pc1.setRemoteDescription({type: 'answer', sdp: modifiedSdp}); 263 264 // The sendonly codec is the only codec in the list of negotiated codecs. 265 const params = transceiver.sender.getParameters(); 266 assert_equals(params.codecs.length, 1, 267 `Only one codec should have been negotiated`); 268 assert_equals(params.codecs[0].payloadType, offeredCodecs[0].payloadType, 269 `The sendonly codec's payloadType shows up in getParameters()`); 270 assert_true(params.codecs[0].sdpFmtpLine == offeredCodecs[0].sdpFmtpLine, 271 `The sendonly codec's sdpFmtpLine shows up in getParameters()`); 272 }, `Offer to send a sendonly H264 codec on a sendonly transceiver`); 273 </script>