Expand test-cases for RTCRtpScriptTransform.
Added test coverage for sendKeyFrameRequest/generateKeyFrame.
Also, some simplifications and code-reuse in existing tests.
Differential Revision: https://phabricator.services.mozilla.com/D179732
bugzilla-url: https://bugzilla.mozilla.org/show_bug.cgi?id=1631263
gecko-commit: ebdc00fb1689551831540db68bf74f4391edbc52
gecko-reviewers: jib
diff --git a/lint.ignore b/lint.ignore
index 457c93c..255a50e 100644
--- a/lint.ignore
+++ b/lint.ignore
@@ -252,6 +252,9 @@
SET TIMEOUT: webauthn/*timeout.https.html
SET TIMEOUT: webdriver/*
SET TIMEOUT: webmessaging/*
+SET TIMEOUT: webrtc-encoded-transform/script-metadata-transform-worker.js
+SET TIMEOUT: webrtc-encoded-transform/script-transform-generateKeyFrame.js
+SET TIMEOUT: webrtc-encoded-transform/script-transform-sendKeyFrameRequest.js
SET TIMEOUT: webstorage/eventTestHarness.js
SET TIMEOUT: webvtt/*
SET TIMEOUT: workers/*
diff --git a/webrtc-encoded-transform/routines.js b/webrtc-encoded-transform/routines.js
index 4db7f39..0d3e2b9 100644
--- a/webrtc-encoded-transform/routines.js
+++ b/webrtc-encoded-transform/routines.js
@@ -1,3 +1,64 @@
+async function getNextMessage(portOrWorker) {
+ return new Promise(resolve => {
+ const resolveWithData = event => resolve(event.data);
+ const rejectWithData = event => reject(event.data);
+ portOrWorker.addEventListener('message', resolveWithData, {once: true});
+ portOrWorker.addEventListener('messageerror', rejectWithData, {once: true});
+ });
+}
+
+
+async function postMethod(port, method, options) {
+ port.postMessage(Object.assign({method}, options));
+ return await getNextMessage(port);
+}
+
+async function createWorker(script) {
+ const worker = new Worker(script);
+ const data = await getNextMessage(worker);
+ assert_equals(data, "registered");
+ return worker;
+}
+
+async function createTransform(worker) {
+ const channel = new MessageChannel;
+ const transform = new RTCRtpScriptTransform(worker, {name:'MockRTCRtpTransform', port: channel.port2}, [channel.port2]);
+ transform.port = channel.port1;
+ channel.port1.start();
+ assert_equals(await getNextMessage(channel.port1), "started");
+ return transform;
+}
+
+async function createTransforms(script) {
+ const worker = await createWorker(script)
+ return Promise.all([createTransform(worker), createTransform(worker)]);
+}
+
+async function createConnectionWithTransform(test, script, gumOptions) {
+ const [senderTransform, receiverTransform] = await createTransforms(script);
+
+ const localStream = await navigator.mediaDevices.getUserMedia(gumOptions);
+
+ let senderPc, receiverPc, sender, receiver;
+
+ await createConnections(test, (firstConnection) => {
+ senderPc = firstConnection;
+ sender = firstConnection.addTrack(localStream.getTracks()[0], localStream);
+ sender.transform = senderTransform;
+ }, (secondConnection) => {
+ receiverPc = secondConnection;
+ secondConnection.ontrack = (trackEvent) => {
+ receiver = trackEvent.receiver;
+ receiver.transform = receiverTransform;
+ };
+ });
+
+ assert_true(!!sender, "sender should be set");
+ assert_true(!!receiver, "receiver should be set");
+
+ return {sender, receiver, senderPc, receiverPc};
+}
+
async function createConnections(test, setupLocalConnection, setupRemoteConnection, doNotCloseAutmoatically) {
const localConnection = new RTCPeerConnection();
const remoteConnection = new RTCPeerConnection();
diff --git a/webrtc-encoded-transform/script-change-transform.https.html b/webrtc-encoded-transform/script-change-transform.https.html
index 9ec82a9..1bb0398 100644
--- a/webrtc-encoded-transform/script-change-transform.https.html
+++ b/webrtc-encoded-transform/script-change-transform.https.html
@@ -37,7 +37,6 @@
const stream = await new Promise((resolve, reject) => {
createConnections(test, (firstConnection) => {
sender = firstConnection.addTrack(localStream.getVideoTracks()[0], localStream);
- firstConnection.getTransceivers()[0].setCodecPreferences([{mimeType: "video/VP8", clockRate: 90000}]);
sender.transform = senderTransform1;
}, (secondConnection) => {
secondConnection.ontrack = (trackEvent) => {
diff --git a/webrtc-encoded-transform/script-metadata-transform-worker.js b/webrtc-encoded-transform/script-metadata-transform-worker.js
index 03ba1f4..40f7e54 100644
--- a/webrtc-encoded-transform/script-metadata-transform-worker.js
+++ b/webrtc-encoded-transform/script-metadata-transform-worker.js
@@ -4,6 +4,21 @@
transformer.reader = transformer.readable.getReader();
transformer.writer = transformer.writable.getWriter();
+ async function waitForDetachAndPostMetadata(frame) {
+ while (true) {
+ if (frame.data.byteLength == 0) {
+ // frame.data has been detached! Verify metadata is still there.
+ self.postMessage({
+ name: `${transformer.options.name} after write`,
+ timestamp: frame.timestamp, type: frame.type,
+ metadata: frame.getMetadata()
+ });
+ return;
+ }
+ await new Promise(r => setTimeout(r, 100));
+ }
+ }
+
let isFirstFrame = true;
function process(transformer)
{
@@ -13,7 +28,13 @@
if (isFirstFrame) {
isFirstFrame = false;
- self.postMessage({ name: transformer.options.name, timestamp: chunk.value.timestamp, metadata: chunk.value.getMetadata() });
+ self.postMessage({
+ name: transformer.options.name,
+ timestamp: chunk.value.timestamp,
+ type: chunk.value.type,
+ metadata: chunk.value.getMetadata()
+ });
+ waitForDetachAndPostMetadata(chunk.value);
}
transformer.writer.write(chunk.value);
process(transformer);
diff --git a/webrtc-encoded-transform/script-metadata-transform.https.html b/webrtc-encoded-transform/script-metadata-transform.https.html
index c565cab..11c88b4 100644
--- a/webrtc-encoded-transform/script-metadata-transform.https.html
+++ b/webrtc-encoded-transform/script-metadata-transform.https.html
@@ -1,16 +1,17 @@
<!doctype html>
<html>
<head>
- <meta charset="utf-8">
-<script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
+ <meta charset='utf-8'>
+ <meta name='timeout' content='long'>
+<script src='/resources/testharness.js'></script>
+ <script src='/resources/testharnessreport.js'></script>
<script src=/resources/testdriver.js></script>
<script src=/resources/testdriver-vendor.js></script>
<script src='../mediacapture-streams/permission-helper.js'></script>
</head>
<body>
- <video id="video1" autoplay></video>
- <script src ="routines.js"></script>
+ <video id='video1' autoplay></video>
+ <script src ='routines.js'></script>
<script>
async function waitForMessage(worker, data)
{
@@ -21,16 +22,16 @@
}
}
-async function gatherMetadata(test, audio)
+async function gatherMetadata(test, kind)
{
worker = new Worker('script-metadata-transform-worker.js');
const data = await new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
- assert_equals(data, "registered");
+ assert_equals(data, 'registered');
// Both audio and vido are needed at one time or another
// so asking for both permissions
await setMediaPermission();
- const localStream = await navigator.mediaDevices.getUserMedia({audio: audio, video: !audio});
+ const localStream = await navigator.mediaDevices.getUserMedia({[kind]: true});
let sender, receiver;
const senderTransform = new RTCRtpScriptTransform(worker, {name:'sender'});
@@ -49,43 +50,251 @@
resolve(trackEvent.streams[0]);
};
});
- test.step_timeout(() => reject("Test timed out"), 5000);
+ test.step_timeout(() => reject('Test timed out'), 5000);
});
- return new Promise((resolve, reject) => {
- let senderMetadata, senderTimestamp;
- worker.onmessage = (event) => {
- if (event.data.name === 'sender') {
- senderMetadata = event.data.metadata;
- senderTimestamp = event.data.timestamp;
- } else if (event.data.name === 'receiver')
- resolve([senderMetadata, senderTimestamp, event.data.metadata, event.data.timestamp]);
- };
- test.step_timeout(() => reject("Metadata test timed out"), 5000);
- });
+ let senderBeforeWrite, senderAfterWrite, receiverBeforeWrite, receiverAfterWrite;
+ while (true) {
+ const {data} = await new Promise(r => worker.onmessage = r);
+ if (data.name == 'sender') {
+ senderBeforeWrite = data;
+ } else if (data.name == 'receiver') {
+ receiverBeforeWrite = data;
+ } else if (data.name == 'sender after write') {
+ senderAfterWrite = data;
+ } else if (data.name == 'receiver after write') {
+ receiverAfterWrite = data;
+ }
+ if (senderBeforeWrite &&
+ senderAfterWrite &&
+ receiverBeforeWrite &&
+ receiverAfterWrite) {
+ return {
+ senderBeforeWrite,
+ senderAfterWrite,
+ receiverBeforeWrite,
+ receiverAfterWrite
+ };
+ }
+ }
}
promise_test(async (test) => {
- const [senderMetadata, senderTimestamp, receiverMetadata, receiverTimestamp] = await gatherMetadata(test, true);
+ const data = await gatherMetadata(test, 'audio');
- assert_equals(senderTimestamp, receiverTimestamp, "timestamp");
- assert_true(!!senderMetadata.synchronizationSource, "ssrc");
- assert_equals(senderMetadata.synchronizationSource, receiverMetadata.synchronizationSource, "ssrc");
- assert_array_equals(senderMetadata.contributingSources, receiverMetadata.contributingSources, "csrc");
-}, "audio exchange with transform");
+ assert_equals(typeof data.senderBeforeWrite.timestamp, 'number');
+ assert_not_equals(data.senderBeforeWrite.timestamp, 0);
+ assert_equals(data.senderBeforeWrite.timestamp,
+ data.senderAfterWrite.timestamp,
+ 'timestamp matches (for sender before and after write)');
+ assert_equals(data.senderBeforeWrite.timestamp,
+ data.receiverBeforeWrite.timestamp,
+ 'timestamp matches (for sender and receiver)');
+ assert_equals(data.receiverBeforeWrite.timestamp,
+ data.receiverAfterWrite.timestamp,
+ 'timestamp matches (for receiver before and after write)');
+}, 'audio metadata: timestamp');
promise_test(async (test) => {
- const [senderMetadata, senderTimestamp, receiverMetadata, receiverTimestamp] = await gatherMetadata(test, false);
+ const data = await gatherMetadata(test, 'audio');
- assert_equals(senderTimestamp, receiverTimestamp, "timestamp");
- assert_true(!!senderMetadata.synchronizationSource, "ssrc");
- assert_equals(senderMetadata.synchronizationSource, receiverMetadata.synchronizationSource, "ssrc");
- assert_array_equals(senderMetadata.contributingSources, receiverMetadata.contributingSources, "csrc");
- assert_equals(senderMetadata.height, receiverMetadata.height, "height");
- assert_equals(senderMetadata.width, receiverMetadata.width, "width");
- assert_equals(senderMetadata.spatialIndex, receiverMetadata.spatialIndex, "spatialIndex");
- assert_equals(senderMetadata.temporalIndex, receiverMetadata.temporalIndex, "temporalIndex");
-}, "video exchange with transform");
+ assert_equals(typeof data.senderBeforeWrite.metadata.synchronizationSource, 'number');
+ assert_not_equals(data.senderBeforeWrite.metadata.synchronizationSource, 0);
+ assert_equals(data.senderBeforeWrite.metadata.synchronizationSource,
+ data.senderAfterWrite.metadata.synchronizationSource,
+ 'ssrc matches (for sender before and after write)');
+ assert_equals(data.senderBeforeWrite.metadata.synchronizationSource,
+ data.receiverBeforeWrite.metadata.synchronizationSource,
+ 'ssrc matches (for sender and receiver)');
+ assert_equals(data.senderBeforeWrite.metadata.synchronizationSource,
+ data.receiverAfterWrite.metadata.synchronizationSource,
+ 'ssrc matches (for receiver before and after write)');
+}, 'audio metadata: synchronizationSource');
+
+promise_test(async (test) => {
+ const data = await gatherMetadata(test, 'audio');
+
+ assert_equals(typeof data.senderBeforeWrite.metadata.payloadType, 'number');
+ assert_equals(data.senderBeforeWrite.metadata.payloadType,
+ data.senderAfterWrite.metadata.payloadType,
+ 'payload type matches (for sender before and after write)');
+ assert_equals(data.senderBeforeWrite.metadata.payloadType,
+ data.receiverBeforeWrite.metadata.payloadType,
+ 'payload type matches (for sender and receiver)');
+ assert_equals(data.senderBeforeWrite.metadata.payloadType,
+ data.receiverAfterWrite.metadata.payloadType,
+ 'payload type matches (for receiver before and after write)');
+}, 'audio metadata: payloadType');
+
+promise_test(async (test) => {
+ const data = await gatherMetadata(test, 'audio');
+
+ assert_array_equals(data.senderBeforeWrite.metadata.contributingSources,
+ data.senderAfterWrite.metadata.contributingSources,
+ 'csrcs are arrays, and match (for sender before and after write)');
+ assert_array_equals(data.senderBeforeWrite.metadata.contributingSources,
+ data.receiverBeforeWrite.metadata.contributingSources,
+ 'csrcs are arrays, and match');
+ assert_array_equals(data.senderBeforeWrite.metadata.contributingSources,
+ data.receiverAfterWrite.metadata.contributingSources,
+ 'csrcs are arrays, and match (for receiver before and after write)');
+}, 'audio metadata: contributingSources');
+
+promise_test(async (test) => {
+ const data = await gatherMetadata(test, 'audio');
+
+ assert_equals(typeof data.receiverBeforeWrite.metadata.sequenceNumber,
+ 'number');
+ assert_equals(data.receiverBeforeWrite.metadata.sequenceNumber,
+ data.receiverAfterWrite.metadata.sequenceNumber,
+ 'sequenceNumber matches (for receiver before and after write)');
+ // spec says sequenceNumber exists only for incoming audio frames
+ assert_equals(data.senderBeforeWrite.metadata.sequenceNumber, undefined);
+ assert_equals(data.senderAfterWrite.metadata.sequenceNumber, undefined);
+}, 'audio metadata: sequenceNumber');
+
+promise_test(async (test) => {
+ const data = await gatherMetadata(test, 'video');
+
+ assert_equals(typeof data.senderBeforeWrite.timestamp, 'number');
+ assert_equals(data.senderBeforeWrite.timestamp,
+ data.senderAfterWrite.timestamp,
+ 'timestamp matches (for sender before and after write)');
+ assert_equals(data.senderBeforeWrite.timestamp,
+ data.receiverBeforeWrite.timestamp,
+ 'timestamp matches (for sender and receiver)');
+ assert_equals(data.senderBeforeWrite.timestamp,
+ data.receiverAfterWrite.timestamp,
+ 'timestamp matches (for receiver before and after write)');
+}, 'video metadata: timestamp');
+
+promise_test(async (test) => {
+ const data = await gatherMetadata(test, 'video');
+
+ assert_equals(typeof data.senderBeforeWrite.metadata.synchronizationSource,
+ 'number');
+ assert_equals(data.senderBeforeWrite.metadata.synchronizationSource,
+ data.senderAfterWrite.metadata.synchronizationSource,
+ 'ssrc matches (for sender before and after write)');
+ assert_equals(data.senderBeforeWrite.metadata.synchronizationSource,
+ data.receiverBeforeWrite.metadata.synchronizationSource,
+ 'ssrc matches (for sender and receiver)');
+ assert_equals(data.senderBeforeWrite.metadata.synchronizationSource,
+ data.receiverAfterWrite.metadata.synchronizationSource,
+ 'ssrc matches (for receiver before and after write)');
+}, 'video metadata: ssrc');
+
+promise_test(async (test) => {
+ const data = await gatherMetadata(test, 'video');
+
+ assert_array_equals(data.senderBeforeWrite.metadata.contributingSources,
+ data.senderAfterWrite.metadata.contributingSources,
+ 'csrcs are arrays, and match (for sender before and after write)');
+ assert_array_equals(data.senderBeforeWrite.metadata.contributingSources,
+ data.receiverBeforeWrite.metadata.contributingSources,
+ 'csrcs are arrays, and match');
+ assert_array_equals(data.senderBeforeWrite.metadata.contributingSources,
+ data.receiverAfterWrite.metadata.contributingSources,
+ 'csrcs are arrays, and match (for receiver before and after write)');
+}, 'video metadata: csrcs');
+
+promise_test(async (test) => {
+ const data = await gatherMetadata(test, 'video');
+
+ assert_equals(typeof data.senderBeforeWrite.metadata.height, 'number');
+ assert_equals(data.senderBeforeWrite.metadata.height,
+ data.senderAfterWrite.metadata.height,
+ 'height matches (for sender before and after write)');
+ assert_equals(data.senderBeforeWrite.metadata.height,
+ data.receiverBeforeWrite.metadata.height,
+ 'height matches (for sender and receiver)');
+ assert_equals(data.senderBeforeWrite.metadata.height,
+ data.receiverAfterWrite.metadata.height,
+ 'height matches (for receiver before and after write)');
+ assert_equals(typeof data.senderBeforeWrite.metadata.width, 'number');
+ assert_equals(data.senderBeforeWrite.metadata.width,
+ data.senderAfterWrite.metadata.width,
+ 'width matches (for sender before and after write)');
+ assert_equals(data.senderBeforeWrite.metadata.width,
+ data.receiverBeforeWrite.metadata.width,
+ 'width matches (for sender and receiver)');
+ assert_equals(data.senderBeforeWrite.metadata.width,
+ data.receiverAfterWrite.metadata.width,
+ 'width matches (for receiver before and after write)');
+}, 'video metadata: width and height');
+
+promise_test(async (test) => {
+ const data = await gatherMetadata(test, 'video');
+
+ assert_equals(typeof data.senderBeforeWrite.metadata.spatialIndex,
+ 'number');
+ assert_equals(data.senderBeforeWrite.metadata.spatialIndex,
+ data.senderAfterWrite.metadata.spatialIndex,
+ 'spatialIndex matches (for sender before and after write)');
+ assert_equals(data.senderBeforeWrite.metadata.spatialIndex,
+ data.receiverBeforeWrite.metadata.spatialIndex,
+ 'spatialIndex matches (for sender and receiver)');
+ assert_equals(data.senderBeforeWrite.metadata.spatialIndex,
+ data.receiverAfterWrite.metadata.spatialIndex,
+ 'spatialIndex matches (for receiver before and after write)');
+ assert_equals(typeof data.senderBeforeWrite.metadata.temporalIndex,
+ 'number');
+ assert_equals(data.senderBeforeWrite.metadata.temporalIndex,
+ data.senderAfterWrite.metadata.temporalIndex,
+ 'temporalIndex matches (for sender before and after write)');
+ assert_equals(data.senderBeforeWrite.metadata.temporalIndex,
+ data.receiverBeforeWrite.metadata.temporalIndex,
+ 'temporalIndex matches (for sender and receiver)');
+ assert_equals(data.senderBeforeWrite.metadata.temporalIndex,
+ data.receiverAfterWrite.metadata.temporalIndex,
+ 'temporalIndex matches (for receiver before and after write)');
+}, 'video metadata: spatial and temporal index');
+
+promise_test(async (test) => {
+ const data = await gatherMetadata(test, 'video');
+
+ assert_array_equals(data.senderBeforeWrite.metadata.dependencies,
+ data.senderAfterWrite.metadata.dependencies,
+ 'dependencies are arrays, and match (for sender before and after write)');
+ assert_array_equals(data.senderBeforeWrite.metadata.dependencies,
+ data.receiverBeforeWrite.metadata.dependencies,
+ 'dependencies are arrays, and match (for sender and receiver)');
+ assert_array_equals(data.senderBeforeWrite.metadata.dependencies,
+ data.receiverAfterWrite.metadata.dependencies,
+ 'dependencies are arrays, and match (for receiver before and after write)');
+}, 'video metadata: dependencies');
+
+promise_test(async (test) => {
+ const data = await gatherMetadata(test, 'video');
+
+ assert_equals(typeof data.senderBeforeWrite.metadata.frameId, 'number');
+ assert_equals(data.senderBeforeWrite.metadata.frameId,
+ data.senderAfterWrite.metadata.frameId,
+ 'frameId matches (for sender before and after write)');
+ assert_equals(data.senderBeforeWrite.metadata.frameId,
+ data.receiverBeforeWrite.metadata.frameId,
+ 'frameId matches (for sender and receiver)');
+ assert_equals(data.senderBeforeWrite.metadata.frameId,
+ data.receiverAfterWrite.metadata.frameId,
+ 'frameId matches (for receiver before and after write)');
+}, 'video metadata: frameId');
+
+promise_test(async (test) => {
+ const data = await gatherMetadata(test, 'video');
+
+ assert_equals(typeof data.senderBeforeWrite.type, 'string');
+ assert_true(data.senderBeforeWrite.type == 'key' || data.senderBeforeWrite.type == 'delta');
+ assert_equals(data.senderBeforeWrite.type,
+ data.senderAfterWrite.type,
+ 'type matches (for sender before and after write)');
+ assert_equals(data.senderBeforeWrite.type,
+ data.receiverBeforeWrite.type,
+ 'type matches (for sender and receiver)');
+ assert_equals(data.senderBeforeWrite.type,
+ data.receiverAfterWrite.type,
+ 'type matches (for receiver before and after write)');
+}, 'video metadata: type');
+
</script>
</body>
</html>
diff --git a/webrtc-encoded-transform/script-transform-generateKeyFrame-simulcast.https.html b/webrtc-encoded-transform/script-transform-generateKeyFrame-simulcast.https.html
new file mode 100644
index 0000000..4174aaf
--- /dev/null
+++ b/webrtc-encoded-transform/script-transform-generateKeyFrame-simulcast.https.html
@@ -0,0 +1,136 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset=utf-8>
+ <title>RTCRtpScriptTransformer.generateKeyFrame simulcast tests</title>
+ <meta name='timeout' content='long'>
+ <script src='/resources/testharness.js'></script>
+ <script src='/resources/testharnessreport.js'></script>
+ <script src=/resources/testdriver.js></script>
+ <script src=/resources/testdriver-vendor.js></script>
+ <script src='../mediacapture-streams/permission-helper.js'></script>
+ </head>
+ <body>
+ <video id='video1' autoplay></video>
+ <video id='video2' autoplay></video>
+ <script src ='routines.js'></script>
+ <script src ='../webrtc/simulcast/simulcast.js'></script>
+ <script src ='../webrtc/RTCPeerConnection-helper.js'></script>
+ <script src='../webrtc/third_party/sdp/sdp.js'></script>
+ <script>
+
+const generateKeyFrame = (port, opts) => postMethod(port, 'generateKeyFrame', opts);
+const waitForFrame = port => postMethod(port, 'waitForFrame');
+
+promise_test(async (test) => {
+ const worker = await createWorker('script-transform-generateKeyFrame.js');
+ const transform = await createTransform(worker);
+ const senderPc = new RTCPeerConnection();
+ const receiverPc = new RTCPeerConnection();
+ // This will only work if first rid is 0
+ exchangeIceCandidates(senderPc, receiverPc);
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ const {sender} = senderPc.addTransceiver(stream.getTracks()[0], {sendEncodings: [{rid: '0'}, {rid: '1'}, {rid: '2'}]});
+ sender.transform = transform;
+ await doOfferToSendSimulcastAndAnswer(senderPc, receiverPc, ['0', '1', '2']);
+
+ let message = await waitForFrame(sender.transform.port);
+ assert_equals(message, 'got frame');
+
+ // Spec says ridless generateKeyFrame selects the first stream, so should work
+ message = await generateKeyFrame(sender.transform.port);
+ assert_equals(message.result, 'success');
+
+ message = await generateKeyFrame(sender.transform.port, {rid: '0'});
+ assert_equals(message.result, 'success');
+
+ message = await generateKeyFrame(sender.transform.port, {rid: '1'});
+ assert_equals(message.result, 'success');
+
+ message = await generateKeyFrame(sender.transform.port, {rid: '2'});
+ assert_equals(message.result, 'success');
+}, 'generateKeyFrame works with simulcast rids');
+
+promise_test(async (test) => {
+ const worker = await createWorker('script-transform-generateKeyFrame.js');
+ const transform = await createTransform(worker);
+ const senderPc = new RTCPeerConnection();
+ const receiverPc = new RTCPeerConnection();
+ // This will only work if first rid is 0
+ exchangeIceCandidates(senderPc, receiverPc);
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ const {sender} = senderPc.addTransceiver(stream.getTracks()[0], {sendEncodings: [{rid: '0'}, {rid: '1'}, {rid: '2'}]});
+ sender.transform = transform;
+ await doOfferToSendSimulcastAndAnswer(senderPc, receiverPc, ['0', '1', '2']);
+
+ let message = await waitForFrame(sender.transform.port);
+ assert_equals(message, 'got frame');
+
+ // Remove the '1' encoding
+ await doOfferToSendSimulcastAndAnswer(senderPc, receiverPc, ['0', '2']);
+
+ // Spec says ridless generateKeyFrame selects the first stream, so should work
+ message = await generateKeyFrame(sender.transform.port);
+ assert_equals(message.result, 'success');
+
+ message = await generateKeyFrame(sender.transform.port, {rid: '0'});
+ assert_equals(message.result, 'success');
+
+ message = await generateKeyFrame(sender.transform.port, {rid: '1'});
+ assert_equals(message.result, 'failure');
+ assert_equals(message.value, 'NotFoundError', `Message: ${message.message}`);
+
+ message = await generateKeyFrame(sender.transform.port, {rid: '2'});
+ assert_equals(message.result, 'success');
+}, 'generateKeyFrame for rid that was negotiated away fails');
+
+promise_test(async (test) => {
+ const worker = await createWorker('script-transform-generateKeyFrame.js');
+ const transform = await createTransform(worker);
+ const senderPc = new RTCPeerConnection();
+ const receiverPc = new RTCPeerConnection();
+ // This will only work if first rid is 0
+ exchangeIceCandidates(senderPc, receiverPc);
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ const {sender} = senderPc.addTransceiver(stream.getTracks()[0], {sendEncodings: [{rid: '0'}, {rid: '1'}, {rid: '2'}]});
+ sender.transform = transform;
+ await doOfferToSendSimulcastAndAnswer(senderPc, receiverPc, ['0', '1', '2']);
+
+ let message = await waitForFrame(sender.transform.port);
+ assert_equals(message, 'got frame');
+
+ // Drop to unicast
+ await doOfferToSendSimulcastAndAnswer(senderPc, receiverPc, []);
+
+ message = await generateKeyFrame(sender.transform.port);
+ assert_equals(message.result, 'success');
+
+ // This is really lame, but there could be frames with rids in flight, and
+ // there's not really any good way to know when they've been flushed out.
+ // If RTCEncodedVideoFrame had a rid field, we might be able to watch for a
+ // frame without a rid. We can't just use generateKeyFrame(null) either,
+ // because a keyframe in flight with the first rid can resolve it. However,
+ // it's reasonable to expect that if we wait for a _second_
+ // generateKeyFrame(null), that should not be resolved with a keyframe for
+ // '0'
+ message = await generateKeyFrame(sender.transform.port);
+ assert_equals(message.result, 'success');
+
+ message = await generateKeyFrame(sender.transform.port, {rid: '0'});
+ assert_equals(message.result, 'failure');
+ assert_equals(message.value, 'NotFoundError', `Message: ${message.message}`);
+
+ message = await generateKeyFrame(sender.transform.port, {rid: '1'});
+ assert_equals(message.result, 'failure');
+ assert_equals(message.value, 'NotFoundError', `Message: ${message.message}`);
+
+ message = await generateKeyFrame(sender.transform.port, {rid: '2'});
+ assert_equals(message.result, 'failure');
+ assert_equals(message.value, 'NotFoundError', `Message: ${message.message}`);
+}, 'generateKeyFrame with rid after simulcast->unicast negotiation fails');
+
+ </script>
+ </body>
+</html>
+
+
diff --git a/webrtc-encoded-transform/script-transform-generateKeyFrame.https.html b/webrtc-encoded-transform/script-transform-generateKeyFrame.https.html
new file mode 100644
index 0000000..348902e
--- /dev/null
+++ b/webrtc-encoded-transform/script-transform-generateKeyFrame.https.html
@@ -0,0 +1,229 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset=utf-8>
+ <title>RTCRtpScriptTransformer.generateKeyFrame tests</title>
+ <meta name='timeout' content='long'>
+ <script src='/resources/testharness.js'></script>
+ <script src='/resources/testharnessreport.js'></script>
+ <script src=/resources/testdriver.js></script>
+ <script src=/resources/testdriver-vendor.js></script>
+ <script src='../mediacapture-streams/permission-helper.js'></script>
+ </head>
+ <body>
+ <video id='video1' autoplay></video>
+ <video id='video2' autoplay></video>
+ <script src ='routines.js'></script>
+ <script src ='../webrtc/simulcast/simulcast.js'></script>
+ <script src ='../webrtc/RTCPeerConnection-helper.js'></script>
+ <script src='../webrtc/third_party/sdp/sdp.js'></script>
+ <script>
+
+const generateKeyFrame = (port, opts) => postMethod(port, 'generateKeyFrame', opts);
+const waitForFrame = port => postMethod(port, 'waitForFrame');
+
+promise_test(async (test) => {
+ const {sender, receiver} = await createConnectionWithTransform(test, 'script-transform-generateKeyFrame.js', {audio: true});
+ let message = await waitForFrame(sender.transform.port);
+ assert_equals(message, 'got frame');
+
+ // No rids
+ message = await generateKeyFrame(sender.transform.port);
+ assert_equals(message.result, 'failure');
+ assert_equals(message.value, 'InvalidStateError', `Message: ${message.message}`);
+
+ message = await waitForFrame(receiver.transform.port);
+ assert_equals(message, 'got frame');
+
+ // No rids
+ message = await generateKeyFrame(receiver.transform.port);
+ assert_equals(message.result, 'failure');
+ assert_equals(message.value, 'InvalidStateError', `Message: ${message.message}`);
+}, 'generateKeyFrame() throws for audio');
+
+promise_test(async (test) => {
+ const {sender, receiver} = await createConnectionWithTransform(test, 'script-transform-generateKeyFrame.js', {video: true});
+ let message = await waitForFrame(sender.transform.port);
+ assert_equals(message, 'got frame');
+
+ // No rids
+ message = await generateKeyFrame(sender.transform.port);
+ assert_equals(message.result, 'success');
+ // value should be a timestamp
+ assert_equals(typeof message.value, 'number');
+ assert_greater_than(message.value, 0);
+
+ // No rids
+ message = await generateKeyFrame(receiver.transform.port);
+ assert_equals(message.result, 'failure');
+ assert_equals(message.value, 'InvalidStateError', `Message: ${message.message}`);
+
+ video1.srcObject = new MediaStream([receiver.track]);
+ await video1.play();
+}, 'generateKeyFrame(null) resolves for video sender, and throws for video receiver');
+
+promise_test(async (test) => {
+ const {sender, receiver} = await createConnectionWithTransform(test, 'script-transform-generateKeyFrame.js', {video: true});
+ let message = await waitForFrame(sender.transform.port);
+ assert_equals(message, 'got frame');
+
+ // Invalid rid, empty string
+ message = await generateKeyFrame(sender.transform.port, {rid: ''});
+ assert_equals(message.result, 'failure');
+ assert_equals(message.value, 'NotAllowedError', `Message: ${message.message}`);
+
+ // Invalid rid, bad ASCII characters
+ message = await generateKeyFrame(sender.transform.port, {rid: '!?'});
+ assert_equals(message.result, 'failure');
+ assert_equals(message.value, 'NotAllowedError', `Message: ${message.message}`);
+
+ // Invalid rid, bad ASCII characters (according to RFC 8852, but not RFC 8851)
+ message = await generateKeyFrame(sender.transform.port, {rid: 'foo-bar'});
+ assert_equals(message.result, 'failure');
+ assert_equals(message.value, 'NotAllowedError', `Message: ${message.message}`);
+
+ // Invalid rid, bad ASCII characters (according to RFC 8852, but not RFC 8851)
+ message = await generateKeyFrame(sender.transform.port, {rid: 'foo_bar'});
+ assert_equals(message.result, 'failure');
+ assert_equals(message.value, 'NotAllowedError', `Message: ${message.message}`);
+
+ // Invalid rid, bad non-ASCII characters
+ message = await generateKeyFrame(sender.transform.port, {rid: '(╯°□°)╯︵ ┻━┻'});
+ assert_equals(message.result, 'failure');
+ assert_equals(message.value, 'NotAllowedError', `Message: ${message.message}`);
+
+ // Invalid rid, too long
+ message = await generateKeyFrame(sender.transform.port, {rid: 'a'.repeat(256)});
+ assert_equals(message.result, 'failure');
+ assert_equals(message.value, 'NotAllowedError', `Message: ${message.message}`);
+}, 'generateKeyFrame throws NotAllowedError for invalid rid');
+
+promise_test(async (test) => {
+ const {sender, receiver} = await createConnectionWithTransform(test, 'script-transform-generateKeyFrame.js', {video: true});
+ let message = await waitForFrame(sender.transform.port);
+ assert_equals(message, 'got frame');
+
+ message = await generateKeyFrame(sender.transform.port, {rid: 'foo'});
+ assert_equals(message.result, 'failure');
+ assert_equals(message.value, 'NotFoundError', `Message: ${message.message}`);
+}, 'generateKeyFrame throws NotFoundError for unknown rid');
+
+promise_test(async (test) => {
+ const {sender, receiver} = await createConnectionWithTransform(test, 'script-transform-generateKeyFrame.js', {video: true});
+ let message = await waitForFrame(sender.transform.port);
+ assert_equals(message, 'got frame');
+
+ message = await generateKeyFrame(sender.transform.port);
+ assert_equals(message.result, 'success');
+
+ const senderTransform = sender.transform;
+ sender.transform = null;
+
+ message = await generateKeyFrame(senderTransform.port);
+ assert_equals(message.result, 'failure');
+ assert_equals(message.value, 'InvalidStateError', `Message: ${message.message}`);
+}, 'generateKeyFrame throws for unset transforms');
+
+promise_test(async (test) => {
+ const {sender, receiver} = await createConnectionWithTransform(test, 'script-transform-generateKeyFrame.js', {video: true});
+ let message = await waitForFrame(sender.transform.port);
+ assert_equals(message, 'got frame');
+
+ message = await generateKeyFrame(sender.transform.port);
+ assert_equals(message.result, 'success');
+ // value should be a timestamp
+ assert_equals(typeof message.value, 'number');
+ assert_greater_than(message.value, 0);
+ const timestamp = message.value;
+
+ message = await generateKeyFrame(sender.transform.port);
+ assert_equals(message.result, 'success');
+ // value should be a timestamp
+ assert_equals(typeof message.value, 'number');
+ assert_greater_than(message.value, timestamp);
+}, 'generateKeyFrame timestamp should advance');
+
+promise_test(async (test) => {
+ const {sender, receiver} = await createConnectionWithTransform(test, 'script-transform-generateKeyFrame.js', {video: true});
+ let message = await waitForFrame(sender.transform.port);
+ assert_equals(message, 'got frame');
+
+ message = await generateKeyFrame(sender.transform.port);
+ assert_equals(message.result, 'success');
+ const count = message.count;
+
+ message = await generateKeyFrame(sender.transform.port);
+ assert_equals(message.result, 'success');
+ assert_greater_than(message.count, count);
+}, 'await generateKeyFrame, await generateKeyFrame should see an increase in count of keyframes');
+
+promise_test(async (test) => {
+ const {sender, receiver, senderPc, receiverPc} = await createConnectionWithTransform(test, 'script-transform-generateKeyFrame.js', {video: true});
+ let message = await waitForFrame(sender.transform.port);
+ assert_equals(message, 'got frame');
+
+ message = await generateKeyFrame(sender.transform.port);
+ assert_equals(message.result, 'success');
+
+ senderPc.getTransceivers()[0].direction = 'inactive';
+ await senderPc.setLocalDescription();
+ await receiverPc.setRemoteDescription(senderPc.localDescription);
+ await receiverPc.setLocalDescription();
+ await senderPc.setRemoteDescription(receiverPc.localDescription);
+
+ message = await generateKeyFrame(sender.transform.port);
+ assert_equals(message.result, 'failure');
+ assert_equals(message.value, 'InvalidStateError', `Message: ${message.message}`);
+
+ senderPc.getTransceivers()[0].direction = 'sendonly';
+ await senderPc.setLocalDescription();
+ await receiverPc.setRemoteDescription(senderPc.localDescription);
+ await receiverPc.setLocalDescription();
+ await senderPc.setRemoteDescription(receiverPc.localDescription);
+
+ message = await generateKeyFrame(sender.transform.port);
+ assert_equals(message.result, 'success');
+}, 'generateKeyFrame rejects when the sender is negotiated inactive, and resumes succeeding when negotiated back to active');
+
+promise_test(async (test) => {
+ const {sender, receiver, senderPc, receiverPc} = await createConnectionWithTransform(test, 'script-transform-generateKeyFrame.js', {video: true});
+ let message = await waitForFrame(sender.transform.port);
+ assert_equals(message, 'got frame');
+
+ message = await generateKeyFrame(sender.transform.port);
+ assert_equals(message.result, 'success');
+
+ senderPc.getTransceivers()[0].stop();
+
+ message = await generateKeyFrame(sender.transform.port);
+ assert_equals(message.result, 'failure');
+ assert_equals(message.value, 'InvalidStateError', `Message: ${message.message}`);
+}, 'generateKeyFrame rejects when the sender is stopped, even without negotiation');
+
+promise_test(async (test) => {
+ const {sender, receiver, senderPc, receiverPc} = await createConnectionWithTransform(test, 'script-transform-generateKeyFrame.js', {video: true});
+ let message = await waitForFrame(sender.transform.port);
+ assert_equals(message, 'got frame');
+
+ message = await generateKeyFrame(sender.transform.port);
+ assert_equals(message.result, 'success');
+
+ await senderPc.getTransceivers()[0].sender.replaceTrack(null);
+
+ message = await generateKeyFrame(sender.transform.port);
+ assert_equals(message.result, 'failure');
+ assert_equals(message.value, 'InvalidStateError', `Message: ${message.message}`);
+}, 'generateKeyFrame rejects with a null track');
+
+// TODO: It would be nice to be able to test that pending generateKeyFrame
+// promises are _rejected_ when the transform is unset, or the sender stops
+// sending. However, getting the timing on this right is going to be very hard.
+// While we could stop the processing of frames before calling
+// generateKeyFrame, this would not necessarily help, because generateKeyFrame
+// promises are resolved _before_ enqueueing the frame into |readable|, and
+// right now the spec does not have a high water mark/backpressure on
+// |readable|, so pausing would not necessarily prevent the enqueue.
+ </script>
+ </body>
+</html>
+
diff --git a/webrtc-encoded-transform/script-transform-generateKeyFrame.js b/webrtc-encoded-transform/script-transform-generateKeyFrame.js
new file mode 100644
index 0000000..5e68ee1
--- /dev/null
+++ b/webrtc-encoded-transform/script-transform-generateKeyFrame.js
@@ -0,0 +1,70 @@
+onrtctransform = event => {
+ const transformer = event.transformer;
+ let keyFrameCount = 0;
+ let gotFrame;
+
+ transformer.options.port.onmessage = event => {
+ const {method, rid} = event.data;
+ // Maybe refactor to have transaction ids?
+ if (method == 'generateKeyFrame') {
+ generateKeyFrame(rid);
+ } else if (method == 'waitForFrame') {
+ waitForFrame();
+ }
+ }
+
+ async function rejectInMs(timeout) {
+ return new Promise((_, reject) => {
+ const rejectWithTimeout = () => {
+ reject(new DOMException(`Timed out after waiting for ${timeout} ms`,
+ 'TimeoutError'));
+ };
+ setTimeout(rejectWithTimeout, timeout);
+ });
+ }
+
+ async function generateKeyFrame(rid) {
+ try {
+ const timestamp = await Promise.race([transformer.generateKeyFrame(rid), rejectInMs(8000)]);
+ transformer.options.port.postMessage({result: 'success', value: timestamp, count: keyFrameCount});
+ } catch (e) {
+ // TODO: This does not work if we send e.name, why?
+ transformer.options.port.postMessage({result: 'failure', value: `${e.name}`, message: `${e.message}`});
+ }
+ }
+
+ async function waitForFrame() {
+ try {
+ await Promise.race([new Promise(r => gotFrameCallback = r), rejectInMs(8000)]);
+ transformer.options.port.postMessage('got frame');
+ } catch (e) {
+ // TODO: This does not work if we send e.name, why?
+ transformer.options.port.postMessage({result: 'failure', value: `${e.name}`, message: `${e.message}`});
+ }
+ }
+
+ transformer.options.port.postMessage('started');
+ transformer.reader = transformer.readable.getReader();
+ transformer.writer = transformer.writable.getWriter();
+
+ function process(transformer)
+ {
+ transformer.reader.read().then(chunk => {
+ if (chunk.done)
+ return;
+ if (chunk.value instanceof RTCEncodedVideoFrame) {
+ if (chunk.value.type == 'key') {
+ keyFrameCount++;
+ }
+ }
+ if (gotFrameCallback) {
+ gotFrameCallback();
+ }
+ transformer.writer.write(chunk.value);
+ process(transformer);
+ });
+ }
+
+ process(transformer);
+};
+self.postMessage('registered');
diff --git a/webrtc-encoded-transform/script-transform-sendKeyFrameRequest.https.html b/webrtc-encoded-transform/script-transform-sendKeyFrameRequest.https.html
new file mode 100644
index 0000000..51b797e
--- /dev/null
+++ b/webrtc-encoded-transform/script-transform-sendKeyFrameRequest.https.html
@@ -0,0 +1,110 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset=utf-8>
+ <title>RTCRtpScriptTransformer.sendKeyFrameRequest tests</title>
+ <meta name='timeout' content='long'>
+ <script src='/resources/testharness.js'></script>
+ <script src='/resources/testharnessreport.js'></script>
+ <script src=/resources/testdriver.js></script>
+ <script src=/resources/testdriver-vendor.js></script>
+ <script src='../mediacapture-streams/permission-helper.js'></script>
+ </head>
+ <body>
+ <video id='video1' autoplay></video>
+ <video id='video2' autoplay></video>
+ <script src ='routines.js'></script>
+ <script>
+
+const sendKeyFrameRequest = (port, opts) => postMethod(port, 'sendKeyFrameRequest', opts);
+const waitForFrame = port => postMethod(port, 'waitForFrame');
+
+promise_test(async (test) => {
+ const {sender, receiver} = await createConnectionWithTransform(test, 'script-transform-sendKeyFrameRequest.js', {video: true});
+ assert_equals(await waitForFrame(receiver.transform.port), 'got frame');
+
+ assert_equals(await sendKeyFrameRequest(receiver.transform.port), 'success');
+
+ assert_equals(await sendKeyFrameRequest(sender.transform.port), 'failure: InvalidStateError');
+
+ video1.srcObject = new MediaStream([receiver.track]);
+ await video1.play();
+}, 'sendKeyFrameRequest resolves for video receiver, and throws for video sender');
+
+promise_test(async (test) => {
+ const {sender, receiver} = await createConnectionWithTransform(test, 'script-transform-sendKeyFrameRequest.js', {audio: true});
+ assert_equals(await waitForFrame(receiver.transform.port), 'got frame');
+
+ assert_equals(await sendKeyFrameRequest(receiver.transform.port), 'failure: InvalidStateError');
+
+ assert_equals(await waitForFrame(sender.transform.port), 'got frame');
+
+ assert_equals(await sendKeyFrameRequest(sender.transform.port), 'failure: InvalidStateError');
+
+ video1.srcObject = new MediaStream([receiver.track]);
+ await video1.play();
+}, 'sendKeyFrameRequest throws for audio sender/receiver');
+
+promise_test(async (test) => {
+ const [senderTransform] = await createTransforms('script-transform-sendKeyFrameRequest.js');
+ assert_equals(await sendKeyFrameRequest(senderTransform.port), 'failure: InvalidStateError');
+}, 'sendKeyFrameRequest throws for unused transforms');
+
+promise_test(async (test) => {
+ const {sender, receiver} = await createConnectionWithTransform(test, 'script-transform-sendKeyFrameRequest.js', {video: true});
+ assert_equals(await waitForFrame(receiver.transform.port), 'got frame');
+
+ const receiverTransform = receiver.transform;
+ assert_equals(await sendKeyFrameRequest(receiverTransform.port), 'success');
+
+ // TODO: Spec currently says that this will immediately cause the transformer
+ // to stop working. This may change.
+ receiver.transform = null;
+
+ assert_equals(await sendKeyFrameRequest(receiverTransform.port), 'failure: InvalidStateError');
+}, 'sendKeyFrameRequest throws for unset transforms');
+
+promise_test(async (test) => {
+ const {sender, receiver, senderPc, receiverPc} = await createConnectionWithTransform(test, 'script-transform-sendKeyFrameRequest.js', {video: true});
+ assert_equals(await waitForFrame(receiver.transform.port), 'got frame');
+
+ assert_equals(await sendKeyFrameRequest(receiver.transform.port), 'success');
+
+ senderPc.getTransceivers()[0].direction = 'inactive';
+ await senderPc.setLocalDescription();
+ await receiverPc.setRemoteDescription(senderPc.localDescription);
+ await receiverPc.setLocalDescription();
+ await senderPc.setRemoteDescription(receiverPc.localDescription);
+
+ assert_equals(await sendKeyFrameRequest(receiver.transform.port), 'failure: InvalidStateError');
+
+ senderPc.getTransceivers()[0].direction = 'sendonly';
+ await senderPc.setLocalDescription();
+ await receiverPc.setRemoteDescription(senderPc.localDescription);
+ await receiverPc.setLocalDescription();
+ await senderPc.setRemoteDescription(receiverPc.localDescription);
+
+ assert_equals(await sendKeyFrameRequest(receiver.transform.port), 'success');
+}, 'sendKeyFrameRequest rejects when the receiver is negotiated inactive, and resumes succeeding when negotiated back to active');
+
+promise_test(async (test) => {
+ const {sender, receiver, senderPc, receiverPc} = await createConnectionWithTransform(test, 'script-transform-sendKeyFrameRequest.js', {video: true});
+ assert_equals(await waitForFrame(receiver.transform.port), 'got frame');
+
+ assert_equals(await sendKeyFrameRequest(receiver.transform.port), 'success');
+
+ receiverPc.getTransceivers()[0].stop();
+
+ assert_equals(await sendKeyFrameRequest(receiver.transform.port), 'failure: InvalidStateError');
+}, 'sendKeyFrameRequest rejects when the receiver is stopped');
+
+// Testing that sendKeyFrameRequest actually results in the sending of keyframe
+// requests is effectively impossible, because there is no API to expose the
+// reception of a keyframe request, keyframes are sent regularly anyway, and
+// the spec allows the receiver to ignore these calls if sending a keyframe
+// request is 'not deemed appropriate'! sendKeyFrameRequest is at most a
+// suggestion.
+
+ </script>
+ </body>
+</html>
diff --git a/webrtc-encoded-transform/script-transform-sendKeyFrameRequest.js b/webrtc-encoded-transform/script-transform-sendKeyFrameRequest.js
new file mode 100644
index 0000000..361d7ce
--- /dev/null
+++ b/webrtc-encoded-transform/script-transform-sendKeyFrameRequest.js
@@ -0,0 +1,63 @@
+onrtctransform = event => {
+ const transformer = event.transformer;
+ let gotFrame;
+
+ transformer.options.port.onmessage = event => {
+ const {method} = event.data;
+ if (method == 'sendKeyFrameRequest') {
+ sendKeyFrameRequest();
+ } else if (method == 'waitForFrame') {
+ waitForFrame();
+ }
+ }
+
+ async function rejectInMs(timeout) {
+ return new Promise((_, reject) => {
+ const rejectWithTimeout = () => {
+ reject(new DOMException(`Timed out after waiting for ${timeout} ms`,
+ 'TimeoutError'));
+ };
+ setTimeout(rejectWithTimeout, timeout);
+ });
+ }
+
+ async function sendKeyFrameRequest() {
+ try {
+ await Promise.race([transformer.sendKeyFrameRequest(), rejectInMs(8000)]);;
+ transformer.options.port.postMessage('success');
+ } catch (e) {
+ // TODO: This does not work if we send e.name, why?
+ transformer.options.port.postMessage(`failure: ${e.name}`);
+ }
+ }
+
+ async function waitForFrame() {
+ try {
+ await Promise.race([new Promise(r => gotFrameCallback = r), rejectInMs(8000)]);
+ transformer.options.port.postMessage('got frame');
+ } catch (e) {
+ // TODO: This does not work if we send e.name, why?
+ transformer.options.port.postMessage({result: 'failure', value: `${e.name}`, message: `${e.message}`});
+ }
+ }
+
+ transformer.options.port.postMessage('started');
+ transformer.reader = transformer.readable.getReader();
+ transformer.writer = transformer.writable.getWriter();
+
+ function process(transformer)
+ {
+ transformer.reader.read().then(chunk => {
+ if (chunk.done)
+ return;
+ if (gotFrameCallback) {
+ gotFrameCallback();
+ }
+ transformer.writer.write(chunk.value);
+ process(transformer);
+ });
+ }
+
+ process(transformer);
+};
+self.postMessage('registered');
diff --git a/webrtc-encoded-transform/script-transform-worker.js b/webrtc-encoded-transform/script-transform-worker.js
index 5ea99cd..88efb9c 100644
--- a/webrtc-encoded-transform/script-transform-worker.js
+++ b/webrtc-encoded-transform/script-transform-worker.js
@@ -1,8 +1,12 @@
onrtctransform = (event) => {
const transformer = event.transformer;
- transformer.options.port.onmessage = (event) => transformer.options.port.postMessage(event.data);
+ transformer.options.port.onmessage = (event) => {
+ if (event.data == "ping") {
+ transformer.options.port.postMessage("pong");
+ }
+ };
- self.postMessage("started");
+ transformer.options.port.postMessage("started");
transformer.reader = transformer.readable.getReader();
transformer.writer = transformer.writable.getWriter();
@@ -11,10 +15,14 @@
transformer.reader.read().then(chunk => {
if (chunk.done)
return;
- if (chunk.value instanceof RTCEncodedVideoFrame)
- self.postMessage("video chunk");
+ if (chunk.value instanceof RTCEncodedVideoFrame) {
+ transformer.options.port.postMessage("video chunk");
+ if (chunk.value.type == "key") {
+ transformer.options.port.postMessage("video keyframe");
+ }
+ }
else if (chunk.value instanceof RTCEncodedAudioFrame)
- self.postMessage("audio chunk");
+ transformer.options.port.postMessage("audio chunk");
transformer.writer.write(chunk.value);
process(transformer);
});
diff --git a/webrtc-encoded-transform/script-transform.https.html b/webrtc-encoded-transform/script-transform.https.html
index e02982f..491e917 100644
--- a/webrtc-encoded-transform/script-transform.https.html
+++ b/webrtc-encoded-transform/script-transform.https.html
@@ -13,140 +13,47 @@
<video id="video2" autoplay></video>
<script src ="routines.js"></script>
<script>
-async function waitForMessage(worker, data)
-{
- while (true) {
- const received = await new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
- if (data === received)
- return;
- }
-}
promise_test(async (test) => {
- worker = new Worker('script-transform-worker.js');
- const data = await new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
- assert_equals(data, "registered");
-
- const channel = new MessageChannel;
- const transform = new RTCRtpScriptTransform(worker, {name:'MockRTCRtpTransform', port: channel.port2}, [channel.port2]);
- transform.port = channel.port1;
- const promise = new Promise(resolve => transform.port.onmessage = (event) => resolve(event.data));
- transform.port.postMessage("test");
- assert_equals(await promise, "test");
+ const worker = await createWorker('script-transform-worker.js');
+ const transform = await createTransform(worker);
+ transform.port.postMessage("ping");
+ assert_equals(await getNextMessage(transform.port), "pong");
}, "transform messaging");
promise_test(async (test) => {
- worker = new Worker('script-transform-worker.js');
- const data = await new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
- assert_equals(data, "registered");
+ const {sender, receiver, senderPc, receiverPc} = await createConnectionWithTransform(test, 'script-transform-worker.js', {audio: true});
- const pc = new RTCPeerConnection();
+ const sender2 = senderPc.addTransceiver('video').sender;
+ const receiver2 = senderPc.getReceivers()[1];
- const senderChannel = new MessageChannel;
- const receiverChannel = new MessageChannel;
- const senderTransform = new RTCRtpScriptTransform(worker, {name:'MockRTCRtpTransform', port: senderChannel.port2}, [senderChannel.port2]);
- const receiverTransform = new RTCRtpScriptTransform(worker, {name:'MockRTCRtpTransform', port: receiverChannel.port2}, [receiverChannel.port2]);
- senderTransform.port = senderChannel.port1;
- receiverTransform.port = receiverChannel.port1;
+ assert_throws_dom("InvalidStateError", () => sender2.transform = sender.transform);
+ assert_throws_dom("InvalidStateError", () => receiver2.transform = receiver.transform);
- const sender1 = pc.addTransceiver('audio').sender;
- const sender2 = pc.addTransceiver('video').sender;
- const receiver1 = pc.getReceivers()[0];
- const receiver2 = pc.getReceivers()[1];
+ sender.transform = sender.transform;
+ receiver.transform = receiver.transform;
- sender1.transform = senderTransform;
- receiver1.transform = receiverTransform;
- assert_throws_dom("InvalidStateError", () => sender2.transform = senderTransform);
- assert_throws_dom("InvalidStateError", () => receiver2.transform = receiverTransform);
-
- sender1.transform = senderTransform;
- receiver1.transform = receiverTransform;
-
- sender1.transform = null;
- receiver1.transform = null;
+ sender.transform = null;
+ receiver.transform = null;
}, "Cannot reuse attached transforms");
promise_test(async (test) => {
- worker = new Worker('script-transform-worker.js');
- const data = await new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
- assert_equals(data, "registered");
- // Video is needed in a later test, so we ask for both permissions
- await setMediaPermission();
- const localStream = await navigator.mediaDevices.getUserMedia({audio: true});
+ const {sender, receiver, senderPc, receiverPc} = await createConnectionWithTransform(test, 'script-transform-worker.js', {audio: true});
+ assert_equals(await getNextMessage(sender.transform.port), "audio chunk");
- const senderChannel = new MessageChannel;
- const receiverChannel = new MessageChannel;
- let sender, receiver;
- const senderTransform = new RTCRtpScriptTransform(worker, {name:'MockRTCRtpTransform', port: senderChannel.port2}, [senderChannel.port2]);
- const receiverTransform = new RTCRtpScriptTransform(worker, {name:'MockRTCRtpTransform', port: receiverChannel.port2}, [receiverChannel.port2]);
- senderTransform.port = senderChannel.port1;
- receiverTransform.port = receiverChannel.port1;
-
- const startedPromise = new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
-
- const stream = await new Promise((resolve, reject) => {
- createConnections(test, (firstConnection) => {
- pc1 = firstConnection;
- sender = firstConnection.addTrack(localStream.getAudioTracks()[0], localStream);
- sender.transform = senderTransform;
- }, (secondConnection) => {
- pc2 = secondConnection;
- secondConnection.ontrack = (trackEvent) => {
- receiver = trackEvent.receiver;
- receiver.transform = receiverTransform;
- resolve(trackEvent.streams[0]);
- };
- });
- test.step_timeout(() => reject("Test timed out"), 5000);
- });
-
- assert_equals(await startedPromise, "started");
-
- await waitForMessage(worker, "audio chunk");
-
- video1.srcObject = stream;
- await video1.play();
+ video1.srcObject = new MediaStream([receiver.track]);
+ await video1.play();
}, "audio exchange with transform");
promise_test(async (test) => {
- worker = new Worker('script-transform-worker.js');
- const data = await new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
- assert_equals(data, "registered");
+ const {sender, receiver, senderPc, receiverPc} = await createConnectionWithTransform(test, 'script-transform-worker.js', {video: true});
- const localStream = await navigator.mediaDevices.getUserMedia({video: true});
+ assert_equals(await getNextMessage(sender.transform.port), "video chunk");
+ // First frame should be a keyframe
+ assert_equals(await getNextMessage(sender.transform.port), "video keyframe");
- const senderChannel = new MessageChannel;
- const receiverChannel = new MessageChannel;
- let sender, receiver;
- const senderTransform = new RTCRtpScriptTransform(worker, {name:'MockRTCRtpTransform', port: senderChannel.port2}, [senderChannel.port2]);
- const receiverTransform = new RTCRtpScriptTransform(worker, {name:'MockRTCRtpTransform', port: receiverChannel.port2}, [receiverChannel.port2]);
- senderTransform.port = senderChannel.port1;
- receiverTransform.port = receiverChannel.port1;
-
- const startedPromise = new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
-
- const stream = await new Promise((resolve, reject) => {
- createConnections(test, (firstConnection) => {
- pc1 = firstConnection;
- sender = firstConnection.addTrack(localStream.getVideoTracks()[0], localStream);
- sender.transform = senderTransform;
- }, (secondConnection) => {
- pc2 = secondConnection;
- secondConnection.ontrack = (trackEvent) => {
- receiver = trackEvent.receiver;
- receiver.transform = receiverTransform;
- resolve(trackEvent.streams[0]);
- };
- });
- test.step_timeout(() => reject("Test timed out"), 5000);
- });
-
- assert_equals(await startedPromise, "started");
-
- await waitForMessage(worker, "video chunk");
-
- video1.srcObject = stream;
- await video1.play();
+ video1.srcObject = new MediaStream([receiver.track]);
+ await video1.play();
}, "video exchange with transform");
</script>
</body>
diff --git a/webrtc/simulcast/setParameters-active.https.html b/webrtc/simulcast/setParameters-active.https.html
index dbe162c..5419105 100644
--- a/webrtc/simulcast/setParameters-active.https.html
+++ b/webrtc/simulcast/setParameters-active.https.html
@@ -12,14 +12,16 @@
<script src="../../mediacapture-streams/permission-helper.js"></script>
<script>
async function queryReceiverStats(pc) {
- const inboundStats = [];
- await Promise.all(pc.getReceivers().map(async receiver => {
+ const inboundStats =
+ await Promise.all(pc.getReceivers().map(async receiver => {
const receiverStats = await receiver.getStats();
+ let inboundStat;
receiverStats.forEach(stat => {
if (stat.type === 'inbound-rtp') {
- inboundStats.push(stat);
+ inboundStat = stat;
}
});
+ return inboundStat;
}));
return inboundStats.map(s => s.framesDecoded);
}