| <!doctype html> |
| <meta charset=utf-8> |
| <title>RTX codec integrity checks</title> |
| <script src="/resources/testharness.js"></script> |
| <script src="/resources/testharnessreport.js"></script> |
| <script> |
| 'use strict'; |
| |
| // Test unidirectional codec support. |
| // |
| // These tests ensure setCodecPreferences() and SDP negotiation work with |
| // sendonly and recvonly codecs, i.e. using codec values that exist in |
| // RTCRtpSender.getCapabilities() but not RTCRtpReceiver.getCapabilities() or |
| // vice versa. |
| // |
| // While this is not necessarily unique to H264, these tests use H264 because |
| // there are many popular devices where support is unidirectional. If the |
| // prerequisite capabilities are not available to the test it ends in |
| // [PRECONDITION_FAILED] rather than test failures. |
| |
| function containsCodec(codecs, codec) { |
| return codecs.find(c => c.mimeType == codec.mimeType && |
| c.sdpFmtpLine == codec.sdpFmtpLine) != null; |
| } |
| |
| function getCodecsWithDirection(mimeType, direction) { |
| const sendCodecs = RTCRtpSender.getCapabilities('video').codecs.filter( |
| c => c.mimeType == mimeType); |
| const recvCodecs = RTCRtpReceiver.getCapabilities('video').codecs.filter( |
| c => c.mimeType == mimeType); |
| const codecs = []; |
| if (direction == 'sendrecv') { |
| for (const sendCodec of sendCodecs) { |
| if (containsCodec(recvCodecs, sendCodec)) { |
| codecs.push(sendCodec); |
| } |
| } |
| } else if (direction == 'sendonly') { |
| for (const sendCodec of sendCodecs) { |
| if (!containsCodec(recvCodecs, sendCodec)) { |
| codecs.push(sendCodec); |
| } |
| } |
| } else if (direction == 'recvonly') { |
| for (const recvCodec of recvCodecs) { |
| if (!containsCodec(sendCodecs, recvCodec)) { |
| codecs.push(recvCodec); |
| } |
| } |
| } |
| return codecs; |
| } |
| |
| // Returns an array of { mimeType, payloadType, sdpFmtpLine } entries in codec |
| // preference order from the first m-section of the SDP. |
| function parseCodecsFromSdp(sdp) { |
| const codecs = []; |
| const kMLineRegex = /\r\nm=video \d+ [A-Z\/]+ (?<pts>[\d\s]+)\r\n/; |
| const {groups: {pts}} = kMLineRegex.exec(sdp); |
| for (const pt of pts.split(" ")) { |
| const rtpmapRegex = new RegExp(`\r\na=rtpmap:${pt} (?<name>[^ \/]+)\/`); |
| const {groups: {name}} = rtpmapRegex.exec(sdp); |
| const fmtpRegex = new RegExp(`\r\na=fmtp:${pt} (?<sdpFmtpLine>.*)\r\n`); |
| // Guard against there not being an fmtp line. |
| const {groups: {sdpFmtpLine} = {}} = fmtpRegex.exec(sdp) ?? {}; |
| const codec = { mimeType: `video/${name}`, payloadType: pt, sdpFmtpLine }; |
| codecs.push(codec); |
| } |
| return codecs; |
| } |
| |
| function replaceCodecInSdp(sdp, oldCodec, newCodec) { |
| // Replace the payload type in the m=video line. |
| sdp = sdp.replace( |
| new RegExp(`(m=video [ \\dA-Z\/]+)${oldCodec.payloadType}`), |
| `$1${newCodec.payloadType}` |
| ); |
| // Replace the payload type in all rtpmap, fmtp and rtcp-fb lines. |
| sdp = sdp.replaceAll( |
| new RegExp(`(a=(rtpmap|fmtp|rtcp-fb):)${oldCodec.payloadType}`, "g"), |
| `$1${newCodec.payloadType}` |
| ); |
| // Lastly, replace the actual "sdpFmtpLine" string. |
| sdp = sdp.replace(oldCodec.sdpFmtpLine, newCodec.sdpFmtpLine); |
| return sdp; |
| } |
| |
| promise_test(async t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| |
| // We need at least one recvonly codec to test "offer to receive". |
| const recvOnlyCodecs = getCodecsWithDirection('video/H264', 'recvonly'); |
| // A sendrecv codec is used to ensure `pc2` has something to answer with which |
| // makes modifying the SDP answer easier, but because we cannot send the |
| // recvonly codec we have to fake it with remote SDP munging. |
| const sendRecvCodecs = getCodecsWithDirection('video/H264', 'sendrecv'); |
| // If any of the following optional asserts fail the test ends with |
| // [PRECONDITION_FAILED] as opposed to [FAIL]. |
| assert_implements_optional( |
| recvOnlyCodecs.length > 0, |
| `There are no recvonly H264 codecs available in getCapabilities.`); |
| assert_implements_optional( |
| sendRecvCodecs.length > 0, |
| `There are no sendrecv H264 codecs available in getCapabilities.`); |
| const recvOnlyCodec = recvOnlyCodecs[0]; |
| const sendRecvCodec = sendRecvCodecs[0]; |
| |
| const pc1Transceiver = pc1.addTransceiver('video', {direction: 'recvonly'}); |
| pc1Transceiver.setCodecPreferences([recvOnlyCodec, sendRecvCodec]); |
| |
| // Offer to receive. |
| await pc1.setLocalDescription(); |
| const offeredCodecs = parseCodecsFromSdp(pc1.localDescription.sdp); |
| assert_equals(offeredCodecs.length, 2, 'Two codecs should be offered'); |
| assert_equals(offeredCodecs[0].mimeType, 'video/H264'); |
| assert_true(offeredCodecs[0].sdpFmtpLine == recvOnlyCodec.sdpFmtpLine, |
| `The first offered codec's sdpFmtpLine is the recvonly one.`); |
| assert_equals(offeredCodecs[1].mimeType, 'video/H264'); |
| assert_true(offeredCodecs[1].sdpFmtpLine == sendRecvCodec.sdpFmtpLine, |
| `The second offered codec's sdpFmtpLine is the sendrecv one.`); |
| await pc2.setRemoteDescription(pc1.localDescription); |
| |
| // Answer to send. |
| const pc2Transceiver = pc2.getTransceivers()[0]; |
| pc2Transceiver.direction = 'sendonly'; |
| await pc2.setLocalDescription(); |
| // Verify that because we are not capable of sending the first codec, it has |
| // been removed from the SDP answer. |
| const answeredCodecs = parseCodecsFromSdp(pc2.localDescription.sdp); |
| assert_equals(answeredCodecs.length, 1, 'One codec should be answered'); |
| assert_equals(answeredCodecs[0].mimeType, 'video/H264'); |
| assert_true(answeredCodecs[0].sdpFmtpLine == sendRecvCodec.sdpFmtpLine, |
| `The answered codec's sdpFmtpLine is the sendrecv one.`); |
| // Trick `pc1` into thinking `pc2` can send the codec by modifying the SDP. |
| // Receiving media is not testable but this ensures that the SDP is accepted. |
| const modifiedSdp = replaceCodecInSdp( |
| pc2.localDescription.sdp, answeredCodecs[0], offeredCodecs[0]); |
| await pc1.setRemoteDescription({type: 'answer', sdp: modifiedSdp}); |
| }, `Offer to receive a recvonly H264 codec on a recvonly transceiver`); |
| |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| |
| const h264RecvOnlyCodecs = getCodecsWithDirection('video/H264', 'recvonly'); |
| const h264SendOnlyCodecs = getCodecsWithDirection('video/H264', 'sendonly'); |
| const vp8SendRecvCodecs = getCodecsWithDirection('video/VP8', 'sendrecv'); |
| // If any of the following optional asserts fail the test ends with |
| // [PRECONDITION_FAILED] as opposed to [FAIL]. |
| assert_implements_optional( |
| h264RecvOnlyCodecs.length > 0, |
| `There are no recvonly H264 codecs available in getCapabilities.`); |
| assert_implements_optional( |
| vp8SendRecvCodecs.length > 0, |
| `There are no sendrecv VP8 codecs available in getCapabilities.`); |
| // Find a recvonly codec with the required level (3.1) for both sending and |
| // receiving, that has a corresponding sendonly codec with the same profile |
| // but a higher level. If there is such a match, we should be able to use the |
| // lower level of the two for sendrecv. |
| const kProfileLevelIdRegex = |
| /profile-level-id=(?<profile_idc>..)(?<profile_iop>..)(?<level_idc>..)/; |
| const kProfileLevelIdReqLevelRegex = /profile-level-id=....1f/; |
| const h264RecvOnlyReqLevelCodecs = h264RecvOnlyCodecs.filter( |
| codec => codec.sdpFmtpLine.match(kProfileLevelIdReqLevelRegex)); |
| const h264RecvOnlyCodec = h264RecvOnlyReqLevelCodecs.find(recv => { |
| const {groups: { |
| profile_idc: recvProfile, |
| profile_iop: recvConstraints, |
| level_idc: recvLevelIdc, |
| } |
| } = kProfileLevelIdRegex.exec(recv.sdpFmtpLine); |
| const recvLevel = parseInt(recvLevelIdc, 16); |
| return h264SendOnlyCodecs.find(send => { |
| const {groups: { |
| profile_idc: sendProfile, |
| profile_iop: sendConstraints, |
| level_idc: sendLevelIdc, |
| } |
| } = kProfileLevelIdRegex.exec(send.sdpFmtpLine); |
| const sendLevel = parseInt(sendLevelIdc, 16); |
| return sendProfile == recvProfile && |
| sendConstraints == recvConstraints && |
| sendLevelIdc > recvLevelIdc; |
| }); |
| }); |
| assert_implements_optional( |
| h264RecvOnlyCodec != undefined, |
| `No recvonly profile-level-id=....1f that matches a higher level ` + |
| `sendonly codec`); |
| const vp8SendRecvCodec = vp8SendRecvCodecs[0]; |
| |
| const transceiver = pc.addTransceiver('video', {direction: 'sendrecv'}); |
| transceiver.setCodecPreferences([h264RecvOnlyCodec, vp8SendRecvCodec]); |
| |
| await pc.setLocalDescription(); |
| const offeredCodecs = parseCodecsFromSdp(pc.localDescription.sdp); |
| // Even though this H264 codec with its level ID is recvonly, we should still |
| // offer to sendrecv it due to sender capabilities being even greater. |
| assert_equals(offeredCodecs.length, 2, 'Two codecs are offered (H264, VP8).'); |
| assert_equals(offeredCodecs[0].mimeType, 'video/H264', |
| 'The first offered codec is H264.'); |
| assert_true(offeredCodecs[0].sdpFmtpLine == h264RecvOnlyCodec.sdpFmtpLine, |
| 'The offered H264 profile-level-id should match the recvonly ' + |
| 'codec since we expect the sender capability to be even higher.'); |
| assert_equals(offeredCodecs[1].mimeType, 'video/VP8', |
| 'The second offered codec is VP8.'); |
| }, `Offering a recvonly codec on a sendrecv transceiver`); |
| |
| promise_test(async t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| |
| // We need at least one sendonly codec to test "offer to send". |
| const sendOnlyCodecs = getCodecsWithDirection('video/H264', 'sendonly'); |
| // A sendrecv codec is used to ensure `pc2` has something to answer with which |
| // makes modifying the SDP answer easier, but because we cannot receive the |
| // sendonly codec we have to fake it with remote SDP munging. |
| const sendRecvCodecs = getCodecsWithDirection('video/H264', 'sendrecv'); |
| // If any of the following optional asserts fail the test ends with |
| // [PRECONDITION_FAILED] as opposed to [FAIL]. |
| assert_implements_optional( |
| sendOnlyCodecs.length > 0, |
| `There are no sendonly H264 codecs available in getCapabilities.`); |
| assert_implements_optional( |
| sendRecvCodecs.length > 0, |
| `There are no sendrecv H264 codecs available in getCapabilities.`); |
| const sendOnlyCodec = sendOnlyCodecs[0]; |
| const sendRecvCodec = sendRecvCodecs[0]; |
| |
| const transceiver = pc1.addTransceiver('video', {direction: 'sendonly'}); |
| transceiver.setCodecPreferences([sendOnlyCodec, sendRecvCodec]); |
| |
| // Offer to send. |
| await pc1.setLocalDescription(); |
| const offeredCodecs = parseCodecsFromSdp(pc1.localDescription.sdp); |
| assert_equals(offeredCodecs.length, 2, 'Two codecs should be offered'); |
| assert_equals(offeredCodecs[0].mimeType, 'video/H264'); |
| assert_true(offeredCodecs[0].sdpFmtpLine == sendOnlyCodec.sdpFmtpLine, |
| `The first offered codec's sdpFmtpLine is the sendonly one.`); |
| assert_equals(offeredCodecs[1].mimeType, 'video/H264'); |
| assert_true(offeredCodecs[1].sdpFmtpLine == sendRecvCodec.sdpFmtpLine, |
| `The second offered codec's sdpFmtpLine is the sendrecv one.`); |
| await pc2.setRemoteDescription(pc1.localDescription); |
| |
| // Answer to receive. |
| await pc2.setLocalDescription(); |
| // Verify that because we are not capable of receiving the first codec, it has |
| // been removed from the SDP answer. |
| const answeredCodecs = parseCodecsFromSdp(pc2.localDescription.sdp); |
| assert_equals(answeredCodecs.length, 1, 'One codec should be answered'); |
| assert_equals(answeredCodecs[0].mimeType, 'video/H264'); |
| assert_true(answeredCodecs[0].sdpFmtpLine == sendRecvCodec.sdpFmtpLine, |
| `The answered codec's sdpFmtpLine is the sendrecv one.`); |
| // Trick `pc1` into thinking `pc2` can receive the codec by modifying the SDP. |
| const modifiedSdp = replaceCodecInSdp( |
| pc2.localDescription.sdp, answeredCodecs[0], offeredCodecs[0]); |
| await pc1.setRemoteDescription({type: 'answer', sdp: modifiedSdp}); |
| |
| // The sendonly codec is the only codec in the list of negotiated codecs. |
| const params = transceiver.sender.getParameters(); |
| assert_equals(params.codecs.length, 1, |
| `Only one codec should have been negotiated`); |
| assert_equals(params.codecs[0].payloadType, offeredCodecs[0].payloadType, |
| `The sendonly codec's payloadType shows up in getParameters()`); |
| assert_true(params.codecs[0].sdpFmtpLine == offeredCodecs[0].sdpFmtpLine, |
| `The sendonly codec's sdpFmtpLine shows up in getParameters()`); |
| }, `Offer to send a sendonly H264 codec on a sendonly transceiver`); |
| </script> |