| <!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 src="../RTCPeerConnection-helper.js"></script> |
| <script> |
| 'use strict'; |
| |
| const kProfileIdKey = 'profile-id'; |
| const kLevelIdKey = 'level-id'; |
| |
| // The level-id value for Level X.Y is calculated as (X * 10 + Y) * 3. |
| // The lowest Level, 1.0, is thus (1 * 10 + 0) * 3 = 30. |
| const kH265Level1dot0 = '30'; |
| |
| function parseFmtpMap(sdpFmtpLine) { |
| const map = new Map(); |
| // For each entry (semi-colon separated key=value). |
| for (let i = 0; i < sdpFmtpLine.length; ++i) { |
| let entryEnd = sdpFmtpLine.indexOf(';', i); |
| if (entryEnd == -1) { |
| entryEnd = sdpFmtpLine.length; |
| } |
| const entryStr = sdpFmtpLine.substring(i, entryEnd); |
| const keyValue = entryStr.split('='); |
| if (keyValue.length != 2) { |
| throw 'Failed to parse sdpFmtpLine'; |
| } |
| map.set(keyValue[0], keyValue[1]); |
| i = entryEnd; |
| } |
| return map; |
| } |
| |
| function findCodecWithProfileId(codecs, mimeType, profileId) { |
| return codecs.find(codec => { |
| if (codec.mimeType != mimeType) { |
| return false; |
| } |
| return parseFmtpMap(codec.sdpFmtpLine).get(kProfileIdKey) == profileId; |
| }); |
| } |
| |
| // Returns `[h265SendCodec, h265RecvCodec]` or aborts the calling test with |
| // [PRECONDITION_FAILED]. |
| function getH265CodecsOrFailPrecondition() { |
| const h265SendCodec = RTCRtpSender.getCapabilities('video').codecs.find( |
| c => c.mimeType == 'video/H265'); |
| assert_implements_optional( |
| h265SendCodec !== undefined, |
| `H265 is not available for sending.`); |
| |
| const h265SendCodecFmtpMap = parseFmtpMap(h265SendCodec.sdpFmtpLine); |
| const profileId = h265SendCodecFmtpMap.get(kProfileIdKey); |
| assert_not_equals(profileId, undefined, |
| `profile-id is missing from sdpFmtpLine`); |
| |
| const h265RecvCodec = findCodecWithProfileId( |
| RTCRtpReceiver.getCapabilities('video').codecs, 'video/H265', profileId); |
| assert_implements_optional( |
| h265RecvCodec !== undefined, |
| `H265 profile-id=${profileId} is not available for receiving.`); |
| |
| return [h265SendCodec, h265RecvCodec]; |
| } |
| |
| function sdpModifyFmtpLevelId(sdp, newLevelId) { |
| const lines = sdp.split('\r\n'); |
| for (let i = 0; i < lines.length; ++i) { |
| if (!lines[i].startsWith('a=fmtp:')) { |
| continue; |
| } |
| const spaceIndex = lines[i].indexOf(' '); |
| if (spaceIndex == -1) { |
| continue; |
| } |
| const fmtpMap = parseFmtpMap(lines[i].substring(spaceIndex + 1)); |
| if (!fmtpMap.has(kLevelIdKey)) { |
| continue; |
| } |
| fmtpMap.set(kLevelIdKey, newLevelId); |
| const sdpFmtpLine = |
| Array.from(fmtpMap, ([key,value]) => `${key}=${value}`).join(';'); |
| lines[i] = lines[i].substring(0, spaceIndex) + ' ' + sdpFmtpLine; |
| } |
| return lines.join('\r\n'); |
| } |
| |
| promise_test(async t => { |
| const [h265SendCodec, h265RecvCodec] = getH265CodecsOrFailPrecondition(); |
| |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| |
| const pc1Transceiver = pc1.addTransceiver('video', {direction: 'sendonly'}); |
| pc1Transceiver.setCodecPreferences([h265SendCodec]); |
| |
| await pc1.setLocalDescription(); |
| await pc2.setRemoteDescription(pc1.localDescription); |
| await pc2.setLocalDescription(); |
| // Modify SDP to tell `pc1` that `pc2` can only receive level-id=30. |
| await pc1.setRemoteDescription({ |
| type: 'answer', |
| sdp: sdpModifyFmtpLevelId(pc2.localDescription.sdp, kH265Level1dot0) |
| }); |
| |
| // Confirm level-id=30 was negotiated regardless of sender capabilities. |
| const sender = pc1Transceiver.sender; |
| const params = sender.getParameters(); |
| assert_equals(params.codecs.length, 1); |
| const negotiatedFmtpMap = parseFmtpMap(params.codecs[0].sdpFmtpLine); |
| assert_equals(negotiatedFmtpMap.get(kLevelIdKey), kH265Level1dot0); |
| }, `Offer to send H265, answer to receive level-id=30 results in level-id=30`); |
| |
| promise_test(async t => { |
| const [h265SendCodec, h265RecvCodec] = getH265CodecsOrFailPrecondition(); |
| |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| |
| const pc1Transceiver = pc1.addTransceiver('video', {direction: 'recvonly'}); |
| pc1Transceiver.setCodecPreferences([h265RecvCodec]); |
| |
| await pc1.setLocalDescription(); |
| // Modify SDP to tell `pc2` that `pc1` can only receive level-id=30. |
| await pc2.setRemoteDescription({ |
| type: 'offer', |
| sdp: sdpModifyFmtpLevelId(pc1.localDescription.sdp, kH265Level1dot0) |
| }); |
| const [pc2Transceiver] = pc2.getTransceivers(); |
| pc2Transceiver.direction = 'sendonly'; |
| await pc2.setLocalDescription(); |
| await pc1.setRemoteDescription(pc2.localDescription); |
| |
| // Confirm level-id=30 was negotiated regardless of sender capabilities. |
| const sender = pc2Transceiver.sender; |
| const params = sender.getParameters(); |
| assert_equals(params.codecs.length, 1); |
| const negotiatedFmtpMap = parseFmtpMap(params.codecs[0].sdpFmtpLine); |
| assert_equals(negotiatedFmtpMap.get(kLevelIdKey), kH265Level1dot0); |
| // Setting a codec that was negotiated should always work, regardless of the |
| // level-id in sender capabilities. |
| params.encodings[0].codec = params.codecs[0]; |
| await sender.setParameters(params); |
| assert_equals(sender.getParameters().encodings[0].codec.mimeType, |
| params.codecs[0].mimeType); |
| assert_equals(sender.getParameters().encodings[0].codec.sdpFmtpLine, |
| params.codecs[0].sdpFmtpLine); |
| }, `Offer to receive level-id=30 and set codec from getParameters`); |
| |
| promise_test(async t => { |
| const [h265SendCodec, h265RecvCodec] = getH265CodecsOrFailPrecondition(); |
| |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| |
| const pc1Transceiver = pc1.addTransceiver('video', {direction: 'recvonly'}); |
| pc1Transceiver.setCodecPreferences([h265RecvCodec]); |
| |
| await pc1.setLocalDescription(); |
| // Modify SDP to tell `pc2` that `pc1` can only receive level-id=30. |
| await pc2.setRemoteDescription({ |
| type: 'offer', |
| sdp: sdpModifyFmtpLevelId(pc1.localDescription.sdp, kH265Level1dot0) |
| }); |
| const [pc2Transceiver] = pc2.getTransceivers(); |
| pc2Transceiver.direction = 'sendonly'; |
| await pc2.setLocalDescription(); |
| await pc1.setRemoteDescription(pc2.localDescription); |
| |
| // Confirm level-id=30 was negotiated regardless of sender capabilities. |
| const sender = pc2Transceiver.sender; |
| const params = sender.getParameters(); |
| assert_equals(params.codecs.length, 1); |
| const negotiatedFmtpMap = parseFmtpMap(params.codecs[0].sdpFmtpLine); |
| assert_equals(negotiatedFmtpMap.get(kLevelIdKey), kH265Level1dot0); |
| // Setting a codec from getCapabilities should work, even if a lower level-id |
| // was negotiated. |
| params.encodings[0].codec = h265SendCodec; |
| await sender.setParameters(params); |
| assert_equals(sender.getParameters().encodings[0].codec.mimeType, |
| h265SendCodec.mimeType); |
| assert_equals(sender.getParameters().encodings[0].codec.sdpFmtpLine, |
| h265SendCodec.sdpFmtpLine); |
| }, `Offer to receive level-id=30 and set codec from getCapabilities`); |
| </script> |