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);
 }