| <!DOCTYPE html> | |
| <meta charset="utf-8"> | |
| <title>RTCRtpEncodingParameters codec property</title> | |
| <meta name="timeout" content="long"> | |
| <script src="/resources/testharness.js"></script> | |
| <script src="/resources/testharnessreport.js"></script> | |
| <script src="RTCPeerConnection-helper.js"></script> | |
| <script src="third_party/sdp/sdp.js"></script> | |
| <script src="simulcast/simulcast.js"></script> | |
| <script> | |
| 'use strict'; | |
| function arrayEquals(a, b) { | |
| return Array.isArray(a) && Array.isArray(b) && | |
| a.length === b.length && | |
| a.every((val, i) => val === b[i]); | |
| } | |
| async function sleep(timeout) { | |
| return new Promise(resolve => { | |
| step_timeout(() => { | |
| resolve(); | |
| }, timeout); | |
| }); | |
| } | |
| function findFirstCodec(name) { | |
| return RTCRtpReceiver.getCapabilities(name.split('/')[0]).codecs.filter(c => c.mimeType.localeCompare(name, undefined, { sensitivity: 'base' }) === 0)[0]; | |
| } | |
| function codecsNotMatching(mimeType) { | |
| return RTCRtpReceiver.getCapabilities(mimeType.split('/')[0]).codecs.filter(c => c.mimeType.localeCompare(mimeType, undefined, {sensitivity: 'base'}) !== 0); | |
| } | |
| function assertCodecEquals(a, b) { | |
| assert_equals(a.mimeType, b.mimeType); | |
| assert_equals(a.clockRate, b.clockRate); | |
| assert_equals(a.channels, b.channels); | |
| assert_equals(a.sdpFmtpLine, b.sdpFmtpLine); | |
| } | |
| async function codecsForSender(sender) { | |
| const rids = sender.getParameters().encodings.map(e => e.rid); | |
| const stats = await sender.getStats(); | |
| const codecs = [...stats] | |
| .filter(([k, v]) => v.type === 'outbound-rtp') | |
| .sort(([k, v], [k2, v2]) => rids.indexOf(v.rid) - rids.indexOf(v2.rid)) | |
| .map(([k, v]) => stats.get(v.codecId).mimeType); | |
| return codecs; | |
| } | |
| async function waitForAllLayers(t, sender) { | |
| const encodings_count = sender.getParameters().encodings.length; | |
| return step_wait_async(t, async () => { | |
| const stats = await sender.getStats(); | |
| return [...stats] | |
| .filter(([k, v]) => v.type === 'outbound-rtp').length == encodings_count; | |
| }, `Wait for ${encodings_count} layers to start`); | |
| } | |
| function step_wait_async(t, cond, description, timeout=3000, interval=100) { | |
| return new Promise(resolve => { | |
| var timeout_full = timeout * t.timeout_multiplier; | |
| var remaining = Math.ceil(timeout_full / interval); | |
| var wait_for_inner = t.step_func(async () => { | |
| if (await cond()) { | |
| resolve(); | |
| } else { | |
| if(remaining === 0) { | |
| assert(false, "step_wait_async", description, | |
| "Timed out waiting on condition"); | |
| } | |
| remaining--; | |
| await sleep(interval); | |
| wait_for_inner(); | |
| } | |
| }); | |
| wait_for_inner(); | |
| }); | |
| } | |
| promise_test(async t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const { sender } = pc.addTransceiver('audio'); | |
| let param = sender.getParameters(); | |
| let encoding = param.encodings[0]; | |
| assert_equals(encoding.codec, undefined); | |
| }, `Codec should be undefined by default on audio encodings`); | |
| promise_test(async t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const { sender } = pc.addTransceiver('video'); | |
| let param = sender.getParameters(); | |
| let encoding = param.encodings[0]; | |
| assert_equals(encoding.codec, undefined); | |
| }, `Codec should be undefined by default on video encodings`); | |
| promise_test(async t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const opus = findFirstCodec('audio/opus'); | |
| const { sender } = pc.addTransceiver('audio', { | |
| sendEncodings: [{codec: opus}], | |
| }); | |
| let param = sender.getParameters(); | |
| let encoding = param.encodings[0]; | |
| assertCodecEquals(opus, encoding.codec); | |
| }, `Creating an audio sender with addTransceiver and codec should work`); | |
| promise_test(async t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const vp8 = findFirstCodec('video/VP8'); | |
| const { sender } = pc.addTransceiver('video', { | |
| sendEncodings: [{codec: vp8}], | |
| }); | |
| let param = sender.getParameters(); | |
| let encoding = param.encodings[0]; | |
| assertCodecEquals(vp8, encoding.codec); | |
| }, `Creating a video sender with addTransceiver and codec should work`); | |
| promise_test(async t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const opus = findFirstCodec('audio/opus'); | |
| const { sender } = pc.addTransceiver('audio'); | |
| let param = sender.getParameters(); | |
| let encoding = param.encodings[0]; | |
| encoding.codec = opus; | |
| await sender.setParameters(param); | |
| param = sender.getParameters(); | |
| encoding = param.encodings[0]; | |
| assertCodecEquals(opus, encoding.codec); | |
| delete encoding.codec; | |
| await sender.setParameters(param); | |
| param = sender.getParameters(); | |
| encoding = param.encodings[0]; | |
| assert_equals(encoding.codec, undefined); | |
| }, `Setting codec on an audio sender with setParameters should work`); | |
| promise_test(async t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const vp8 = findFirstCodec('video/VP8'); | |
| const { sender } = pc.addTransceiver('video'); | |
| let param = sender.getParameters(); | |
| let encoding = param.encodings[0]; | |
| encoding.codec = vp8; | |
| await sender.setParameters(param); | |
| param = sender.getParameters(); | |
| encoding = param.encodings[0]; | |
| assertCodecEquals(vp8, encoding.codec); | |
| delete encoding.codec; | |
| await sender.setParameters(param); | |
| param = sender.getParameters(); | |
| encoding = param.encodings[0]; | |
| assert_equals(encoding.codec, undefined); | |
| }, `Setting codec on a video sender with setParameters should work`); | |
| promise_test(async t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const newCodec = { | |
| mimeType: "audio/newCodec", | |
| clockRate: 90000, | |
| channel: 2, | |
| }; | |
| assert_throws_dom('OperationError', () => pc.addTransceiver('audio', { | |
| sendEncodings: [{codec: newCodec}], | |
| })); | |
| }, `Creating an audio sender with addTransceiver and non-existing codec should throw OperationError`); | |
| promise_test(async t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const newCodec = { | |
| mimeType: "dummy/newCodec", | |
| clockRate: 90000, | |
| channel: 2, | |
| }; | |
| assert_throws_dom('OperationError', () => pc.addTransceiver('audio', { | |
| sendEncodings: [{codec: newCodec}], | |
| })); | |
| }, `Creating an audio sender with addTransceiver and non-existing codec type should throw OperationError`); | |
| promise_test(async t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const newCodec = { | |
| mimeType: "video/newCodec", | |
| clockRate: 90000, | |
| }; | |
| assert_throws_dom('OperationError', () => pc.addTransceiver('video', { | |
| sendEncodings: [{codec: newCodec}], | |
| })); | |
| }, `Creating a video sender with addTransceiver and non-existing codec should throw OperationError`); | |
| promise_test(async t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const newCodec = { | |
| mimeType: "dummy/newCodec", | |
| clockRate: 90000, | |
| }; | |
| assert_throws_dom('OperationError', () => pc.addTransceiver('video', { | |
| sendEncodings: [{codec: newCodec}], | |
| })); | |
| }, `Creating a video sender with addTransceiver and non-existing codec type should throw OperationError`); | |
| promise_test(async t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const newCodec = { | |
| mimeType: "audio/newCodec", | |
| clockRate: 90000, | |
| channel: 2, | |
| }; | |
| const { sender } = pc.addTransceiver('audio'); | |
| let param = sender.getParameters(); | |
| let encoding = param.encodings[0]; | |
| encoding.codec = newCodec; | |
| await promise_rejects_dom(t, "InvalidModificationError", sender.setParameters(param)); | |
| }, `Setting a non-existing codec on an audio sender with setParameters should throw InvalidModificationError`); | |
| promise_test(async t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const newCodec = { | |
| mimeType: "video/newCodec", | |
| clockRate: 90000, | |
| }; | |
| const { sender } = pc.addTransceiver('video'); | |
| let param = sender.getParameters(); | |
| let encoding = param.encodings[0]; | |
| encoding.codec = newCodec; | |
| await promise_rejects_dom(t, "InvalidModificationError", sender.setParameters(param)); | |
| }, `Setting a non-existing codec on a video sender with setParameters should throw InvalidModificationError`); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc2.close()); | |
| const opus = findFirstCodec('audio/opus'); | |
| const nonOpus = codecsNotMatching(opus.mimeType); | |
| const transceiver = pc1.addTransceiver('audio'); | |
| exchangeIceCandidates(pc1, pc2); | |
| const negotiated = exchangeOfferAnswer(pc1, pc2); | |
| const trackEvent = await new Promise(r => pc2.ontrack = r); | |
| trackEvent.transceiver.setCodecPreferences(nonOpus); | |
| await negotiated; | |
| const sender = transceiver.sender; | |
| let param = sender.getParameters(); | |
| let encoding = param.encodings[0]; | |
| encoding.codec = opus; | |
| await promise_rejects_dom(t, "InvalidModificationError", sender.setParameters(param)); | |
| }, `Setting a non-preferred codec on an audio sender with setParameters should throw InvalidModificationError`); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc2.close()); | |
| const vp8 = findFirstCodec('video/VP8'); | |
| const nonVP8 = codecsNotMatching(vp8.mimeType); | |
| const transceiver = pc1.addTransceiver('video'); | |
| exchangeIceCandidates(pc1, pc2); | |
| const negotiated = exchangeOfferAnswer(pc1, pc2); | |
| const trackEvent = await new Promise(r => pc2.ontrack = r); | |
| trackEvent.transceiver.setCodecPreferences(nonVP8); | |
| await negotiated; | |
| const sender = transceiver.sender; | |
| let param = sender.getParameters(); | |
| let encoding = param.encodings[0]; | |
| encoding.codec = vp8; | |
| await promise_rejects_dom(t, "InvalidModificationError", sender.setParameters(param)); | |
| }, `Setting a non-preferred codec on a video sender with setParameters should throw InvalidModificationError`); | |
| promise_test(async (t) => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| const opus = findFirstCodec('audio/opus'); | |
| const nonOpus = codecsNotMatching(opus.mimeType); | |
| const transceiver = pc1.addTransceiver('audio'); | |
| exchangeIceCandidates(pc1, pc2); | |
| const negotiated = exchangeOfferAnswer(pc1, pc2); | |
| const trackEvent = await new Promise(r => pc2.ontrack = r); | |
| trackEvent.transceiver.setCodecPreferences(nonOpus); | |
| await negotiated; | |
| const sender = transceiver.sender; | |
| let param = sender.getParameters(); | |
| let encoding = param.encodings[0]; | |
| encoding.codec = opus; | |
| await promise_rejects_dom(t, "InvalidModificationError", sender.setParameters(param)); | |
| }, `Setting a non-negotiated codec on an audio sender with setParameters should throw InvalidModificationError`); | |
| promise_test(async (t) => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| const vp8 = findFirstCodec('video/VP8'); | |
| const nonVP8 = codecsNotMatching(vp8.mimeType); | |
| const transceiver = pc1.addTransceiver('video'); | |
| exchangeIceCandidates(pc1, pc2); | |
| const negotiated = exchangeOfferAnswer(pc1, pc2); | |
| const trackEvent = await new Promise(r => pc2.ontrack = r); | |
| trackEvent.transceiver.setCodecPreferences(nonVP8); | |
| await negotiated; | |
| const sender = transceiver.sender; | |
| let param = sender.getParameters(); | |
| let encoding = param.encodings[0]; | |
| encoding.codec = vp8; | |
| await promise_rejects_dom(t, "InvalidModificationError", sender.setParameters(param)); | |
| }, `Setting a non-negotiated codec on a video sender with setParameters should throw InvalidModificationError`); | |
| promise_test(async (t) => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| const opus = findFirstCodec('audio/opus'); | |
| const nonOpus = codecsNotMatching(opus.mimeType); | |
| const transceiver = pc1.addTransceiver('audio', { | |
| sendEncodings: [{codec: opus}], | |
| }); | |
| const sender = transceiver.sender; | |
| exchangeIceCandidates(pc1, pc2); | |
| await exchangeOfferAnswer(pc1, pc2); | |
| let param = sender.getParameters(); | |
| let encoding = param.encodings[0]; | |
| assertCodecEquals(opus, encoding.codec); | |
| pc2.getTransceivers()[0].setCodecPreferences(nonOpus); | |
| await exchangeOfferAnswer(pc1, pc2); | |
| param = sender.getParameters(); | |
| encoding = param.encodings[0]; | |
| assert_equals(encoding.codec, undefined); | |
| }, `Codec should be undefined after negotiating away the currently set codec on an audio sender`); | |
| promise_test(async (t) => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| const vp8 = findFirstCodec('video/VP8'); | |
| const nonVP8 = codecsNotMatching(vp8.mimeType); | |
| const transceiver = pc1.addTransceiver('video', { | |
| sendEncodings: [{codec: vp8}], | |
| }); | |
| const sender = transceiver.sender; | |
| exchangeIceCandidates(pc1, pc2); | |
| await exchangeOfferAnswer(pc1, pc2); | |
| let param = sender.getParameters(); | |
| let encoding = param.encodings[0]; | |
| assertCodecEquals(vp8, encoding.codec); | |
| pc2.getTransceivers()[0].setCodecPreferences(nonVP8); | |
| await exchangeOfferAnswer(pc1, pc2); | |
| param = sender.getParameters(); | |
| encoding = param.encodings[0]; | |
| assert_equals(encoding.codec, undefined); | |
| }, `Codec should be undefined after negotiating away the currently set codec on a video sender`); | |
| promise_test(async (t) => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| const stream = await getNoiseStream({audio:true}); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| const opus = findFirstCodec('audio/opus'); | |
| const nonOpus = codecsNotMatching(opus.mimeType); | |
| const transceiver = pc1.addTransceiver(stream.getTracks()[0]); | |
| const sender = transceiver.sender; | |
| exchangeIceCandidates(pc1, pc2); | |
| const negotiated = exchangeOfferAnswer(pc1, pc2); | |
| const trackEvent = await new Promise(r => pc2.ontrack = r); | |
| trackEvent.transceiver.setCodecPreferences(nonOpus.concat([opus])); | |
| await negotiated; | |
| let codecs = await codecsForSender(sender); | |
| assert_not_equals(codecs[0], opus.mimeType); | |
| let param = sender.getParameters(); | |
| let encoding = param.encodings[0]; | |
| encoding.codec = opus; | |
| await sender.setParameters(param); | |
| await step_wait_async(t, async () => { | |
| let old_codecs = codecs; | |
| codecs = await codecsForSender(sender); | |
| return !arrayEquals(codecs, old_codecs); | |
| }, 'Waiting for current codecs to change', 5000, 200); | |
| assert_array_equals(codecs, [opus.mimeType]); | |
| }, `Stats output-rtp should match the selected codec in non-simulcast usecase on an audio sender`); | |
| promise_test(async (t) => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| const stream = await getNoiseStream({video:true}); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| const vp8 = findFirstCodec('video/VP8'); | |
| const nonVP8 = codecsNotMatching(vp8.mimeType); | |
| const transceiver = pc1.addTransceiver(stream.getTracks()[0]); | |
| const sender = transceiver.sender; | |
| exchangeIceCandidates(pc1, pc2); | |
| const negotiated = exchangeOfferAnswer(pc1, pc2); | |
| const trackEvent = await new Promise(r => pc2.ontrack = r); | |
| trackEvent.transceiver.setCodecPreferences(nonVP8.concat([vp8])); | |
| await negotiated; | |
| let codecs = await codecsForSender(sender); | |
| assert_not_equals(codecs[0], vp8.mimeType); | |
| let param = sender.getParameters(); | |
| let encoding = param.encodings[0]; | |
| encoding.codec = vp8; | |
| await sender.setParameters(param); | |
| await step_wait_async(t, async () => { | |
| let old_codecs = codecs; | |
| codecs = await codecsForSender(sender); | |
| return !arrayEquals(codecs, old_codecs); | |
| }, 'Waiting for current codecs to change', 5000, 200); | |
| assert_array_equals(codecs, [vp8.mimeType]); | |
| }, `Stats output-rtp should match the selected codec in non-simulcast usecase on a video sender`); | |
| promise_test(async (t) => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| const stream = await getNoiseStream({video:true}); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| const vp8 = findFirstCodec('video/VP8'); | |
| const h264 = findFirstCodec('video/H264'); | |
| const transceiver = pc1.addTransceiver(stream.getTracks()[0], { | |
| sendEncodings: [{rid: '0'}, {rid: '1'}, {rid: '2'}], | |
| }); | |
| const sender = transceiver.sender; | |
| exchangeIceCandidates(pc1, pc2); | |
| const negotiated = doOfferToSendSimulcastAndAnswer(pc1, pc2, ['0', '1', '2']); | |
| const trackEvent = await new Promise(r => pc2.ontrack = r); | |
| trackEvent.transceiver.setCodecPreferences([h264, vp8]); | |
| await negotiated; | |
| await waitForAllLayers(t, sender); | |
| let codecs = await codecsForSender(sender); | |
| assert_not_equals(codecs[0], vp8.mimeType); | |
| assert_not_equals(codecs[1], vp8.mimeType); | |
| assert_not_equals(codecs[2], vp8.mimeType); | |
| let param = sender.getParameters(); | |
| param.encodings[0].codec = vp8; | |
| param.encodings[1].codec = vp8; | |
| param.encodings[2].codec = vp8; | |
| await sender.setParameters(param); | |
| // Waiting for 10s as ramp-up time can be slow in the runners. | |
| await step_wait_async(t, async () => { | |
| let old_codecs = codecs; | |
| codecs = await codecsForSender(sender); | |
| return !arrayEquals(codecs, old_codecs); | |
| }, 'Waiting for current codecs to change', 10000, 200); | |
| assert_array_equals(codecs, [vp8.mimeType, vp8.mimeType, vp8.mimeType]); | |
| }, `Stats output-rtp should match the selected codec in simulcast usecase on a video sender`); | |
| promise_test(async (t) => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| const stream = await getNoiseStream({video:true}); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| const vp8 = findFirstCodec('video/VP8'); | |
| const h264 = findFirstCodec('video/H264'); | |
| const transceiver = pc1.addTransceiver(stream.getTracks()[0], { | |
| sendEncodings: [{rid: '0'}, {rid: '1'}, {rid: '2'}], | |
| }); | |
| const sender = transceiver.sender; | |
| exchangeIceCandidates(pc1, pc2); | |
| const negotiated = doOfferToSendSimulcastAndAnswer(pc1, pc2, ['0', '1', '2']); | |
| const trackEvent = await new Promise(r => pc2.ontrack = r); | |
| trackEvent.transceiver.setCodecPreferences([h264, vp8]); | |
| await negotiated; | |
| await waitForAllLayers(t, sender); | |
| let codecs = await codecsForSender(sender); | |
| assert_not_equals(codecs[0], vp8.mimeType); | |
| assert_not_equals(codecs[1], vp8.mimeType); | |
| assert_not_equals(codecs[2], vp8.mimeType); | |
| let param = sender.getParameters(); | |
| param.encodings[1].codec = vp8; | |
| await sender.setParameters(param); | |
| await step_wait_async(t, async () => { | |
| let old_codecs = codecs; | |
| codecs = await codecsForSender(sender); | |
| return !arrayEquals(codecs, old_codecs); | |
| }, 'Waiting for current codecs to change', 5000, 200); | |
| assert_not_equals(codecs[0], vp8.mimeType); | |
| assert_equals(codecs[1], vp8.mimeType); | |
| assert_not_equals(codecs[2], vp8.mimeType); | |
| }, `Stats output-rtp should match the selected mixed codecs in simulcast usecase on a video sender`); | |
| </script> |