274 lines
12 KiB
HTML
274 lines
12 KiB
HTML
<!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';
|
|
|
|
// 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 the
|
|
// order that they appeared in the SDP.
|
|
function parseCodecsFromSdp(sdp) {
|
|
const codecs = [];
|
|
// For each a=rtpmap:...
|
|
const kRtpMapLineRegex = /\r\na=rtpmap:/g;
|
|
for (const match of sdp.matchAll(kRtpMapLineRegex)) {
|
|
const rtpmapIndex = match.index + 11;
|
|
const rtpmapSpaceIndex = sdp.indexOf(' ', rtpmapIndex)
|
|
const payloadType = Number(sdp.slice(rtpmapIndex, rtpmapSpaceIndex));
|
|
const codecName = sdp.slice(rtpmapSpaceIndex + 1,
|
|
sdp.indexOf('/', rtpmapSpaceIndex));
|
|
let sdpFmtpLine = undefined; // Can be undefined e.g. VP8.
|
|
const fmtpLineWithPT = `\r\na=fmtp:${payloadType} `;
|
|
let fmtpIndex = sdp.indexOf(fmtpLineWithPT, rtpmapIndex);
|
|
if (fmtpIndex != -1) {
|
|
fmtpIndex += fmtpLineWithPT.length;
|
|
const fmtpLineEnd = sdp.indexOf('\r\n', fmtpIndex);
|
|
if (fmtpLineEnd == -1) {
|
|
throw 'Parse error: Missing expected end of FMTP line';
|
|
}
|
|
sdpFmtpLine = sdp.slice(fmtpIndex, fmtpLineEnd);
|
|
}
|
|
const codec = { mimeType: `video/${codecName}`, payloadType, sdpFmtpLine };
|
|
codecs.push(codec);
|
|
}
|
|
return codecs;
|
|
}
|
|
|
|
function replaceAllInSubstr(
|
|
str, startIndex, endIndex, pattern, replacement) {
|
|
return str.slice(0, startIndex) +
|
|
str.slice(startIndex, endIndex).replaceAll(pattern, replacement) +
|
|
str.slice(endIndex);
|
|
}
|
|
|
|
function replaceCodecInSdp(sdp, oldCodec, newCodec) {
|
|
// Replace the payload type in the m=video line.
|
|
const mVideoStartIndex = sdp.indexOf('m=video');
|
|
const mVideoEndIndex = sdp.indexOf('\r\n', mVideoStartIndex);
|
|
if (mVideoStartIndex == -1 || mVideoEndIndex == -1) {
|
|
throw 'Failed to parse m=video line in the codec replace algorithm';
|
|
}
|
|
sdp = replaceAllInSubstr(
|
|
sdp, mVideoStartIndex, mVideoEndIndex, String(oldCodec.payloadType),
|
|
String(newCodec.payloadType));
|
|
// Replace the payload type in all the RTP map and FMTP lines.
|
|
const rtpStartIndex = sdp.indexOf('a=rtpmap:' + oldCodec.payloadType);
|
|
const rtpEndIndex = sdp.indexOf(oldCodec.sdpFmtpLine);
|
|
if (rtpStartIndex == -1 || rtpEndIndex == -1) {
|
|
throw 'Failed to parse RTP/FMTP lines in the codec replace algorithm';
|
|
}
|
|
sdp = replaceAllInSubstr(
|
|
sdp, rtpStartIndex, rtpEndIndex, ':' + oldCodec.payloadType,
|
|
':' + 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 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.`);
|
|
// Pick a level (3.1) that we're required to support for both sending and
|
|
// receiving. If such a codec is listed in `h264RecvOnlyCodecs` that means our
|
|
// sender capability has an even greater level.
|
|
const h264RecvOnlyCodec = h264RecvOnlyCodecs.find(
|
|
codec => codec.sdpFmtpLine.includes('profile-level-id=64001f'));
|
|
assert_implements_optional(
|
|
h264RecvOnlyCodec != undefined,
|
|
`profile-level-id=64001f is not exclusive to ` +
|
|
`RTCRtpReceiver.getCapabilities.`);
|
|
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>
|