Skip to content

Commit

Permalink
Add WPTs for negotiating H265 with minimal level-id in SDP answers.
Browse files Browse the repository at this point in the history
Add test coverage for w3c/webrtc-pc#3023 which
unblocks merging the PR (it has "editors can integrate"). Specifically
it should be possible to negotiate H265 even if the level-id is
downgraded in the SDP answer (not throwing on level-id mismatch).

- H265 is behind flags so a virtual test suite is added.
- If bot lacks HW capabilities to do H265, the tests will
  PRECONDITION_FAILED. To avoid reverting tests we will allow the bots
  to both pass and fail, I filed crbug.com/388299759.

Some of the tests FAIL even when they run: passing these tests will be
covered by crbug.com/381407888.

Bug: chromium:381407888, webrtc:41480904, chromium:388299759
Change-Id: Ie017560b4310418a4c9ed854dcb09136b9b446e7
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6148192
Reviewed-by: David Baron <[email protected]>
Commit-Queue: Henrik Boström <[email protected]>
Reviewed-by: Evan Shrubsole <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1404052}
  • Loading branch information
henbos authored and chromium-wpt-export-bot committed Jan 9, 2025
1 parent 5c78b7a commit 545a2ed
Showing 1 changed file with 187 additions and 0 deletions.
187 changes: 187 additions & 0 deletions webrtc/protocol/h265-level-id.https.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
<!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, params.codecs[0]);
}, `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, h265SendCodec);
}, `Offer to receive level-id=30 and set codec from getCapabilities`);
</script>

0 comments on commit 545a2ed

Please sign in to comment.