From 9f0ebece90fc231191ee3bc6cfb7c5e157f4c9ad Mon Sep 17 00:00:00 2001 From: Youenn Fablet Date: Wed, 17 Dec 2025 18:04:12 +0100 Subject: [PATCH 1/6] Introduce MediaStreamTrackHandle --- index.bs | 68 ++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/index.bs b/index.bs index 9f03733..4b6757f 100644 --- a/index.bs +++ b/index.bs @@ -117,14 +117,20 @@ Failing to release them (or waiting for garbage collection) can cause the source ### Interface definition ### {#track-processor-interface}
+[Exposed=(Window,DedicatedWorker), Transferable]
+interface MediaStreamTrackHandle {
+   constructor(MediaStreamTrack track);
+};
+
 [Exposed=DedicatedWorker]
 interface MediaStreamTrackProcessor {
     constructor(MediaStreamTrackProcessorInit init);
     readonly attribute ReadableStream readable;
 };
 
+typedef (MediaStreamTrack or MediaStreamTrackHandle) MediaStreamTrackOrHandle;
 dictionary MediaStreamTrackProcessorInit {
-  required MediaStreamTrack track;
+  required MediaStreamTrackOrHandle track;
   [EnforceRange] unsigned short maxBufferSize;
 };
 
@@ -158,8 +164,12 @@ application that have not yet been handled. MediaStreamTrackProcessor(|init|) -1. If |init|.{{MediaStreamTrackProcessorInit/track}} is not a valid {{MediaStreamTrack}}, +1. If |init|.{{MediaStreamTrackProcessorInit/track}} is a {{MediaStreamTrack}} that is not valid, throw a {{TypeError}}. +1. If |init|.{{MediaStreamTrackProcessorInit/track}} is a {{MediaStreamTrackHandle}}, run the following substeps: + 1. If |init|.{{MediaStreamTrackProcessorInit/track}}.[[transferable]] is false, throw a {{ TypeError}}. + 1. If |init|.{{MediaStreamTrackProcessorInit/track}}.`[[track]]` is referencing a non valid {{MediaStreamTrack}}, throw a {{TypeError}}. + 1. Set |init|.{{MediaStreamTrackProcessorInit/track}}.[[transferable]] to false. 1. Let |maxBufferSize| be 1. 1. If |init|.{{MediaStreamTrackProcessorInit/maxBufferSize}} has an integer value greater than 1, run the following substeps: 1. Set |maxBufferSize| to |init|.{{MediaStreamTrackProcessorInit/maxBufferSize}}. @@ -179,7 +189,7 @@ application that have not yet been handled. ### Attributes ### {#attributes-processor}
readable
-
Allows reading the frames delivered by the {{MediaStreamTrack}} stored +
Allows reading the frames delivered by the {{MediaStreamTrack}} or {{MediaStreamTrackHandle}} stored in the `[[track]]` internal slot. This attribute is created the first time it is invoked according to the following steps: 1. Initialize [=this=].{{MediaStreamTrackProcessor/readable}} to be a new {{ReadableStream}}. @@ -206,10 +216,12 @@ It is defined by running the following steps: The processorClose algorithm is given a |processor| as input. It is defined by running the following steps: 1. If |processor|.`[[isClosed]]` is true, abort these steps. -2. Disconnect |processor| from |processor|.`[[track]]`. The mechanism to do this is UA specific and the result is that |processor| is no longer a sink of |processor|.`[[track]]`. -3. [$ReadableStreamDefaultControllerClose|Close$] |processor|.{{MediaStreamTrackProcessor/readable}}.[=ReadableStream/[[controller]]=]. -4. [=list/Empty=] |processor|.`[[queue]]`. -5. Set |processor|.`[[isClosed]]` to true. +1. Disconnect |processor| from |processor|.`[[track]]`. The mechanism to do this is UA specific and the result is that |processor| is no longer a sink of |processor|.`[[track]]`. +1. If |processor|.`[[track]]` is a {{MediaStreamTrackHandle}}, set |processor|.`[[track]]`.`[[track]` to null. +1. Set |processor|.`[[track]]` to null. +1. [$ReadableStreamDefaultControllerClose|Close$] |processor|.{{MediaStreamTrackProcessor/readable}}.[=ReadableStream/[[controller]]=]. +1. [=list/Empty=] |processor|.`[[queue]]`. +1. Set |processor|.`[[isClosed]]` to true.
@@ -240,6 +252,38 @@ When the `[[track]]` of a {{MediaStreamTrackProcessor}} |processor| [=track|ends=], the [=processorClose=] algorithm must be executed with |processor| as parameter. +### MediaStreamTrackHandle ### + +The {{MediaStreamTrackHandle}} interface represents a proxy to a {{MediaStreamTrack}} object that is useful for making a DedicatedWorkerGlobalScope {{MediaStreamTrackProcessor}} a sink to a {{MediaStreamTrack}} living in another context. + +A {{MediaStreamTrackHandle}} has the following slots: +1. A [[transferable]] internal slot that is used to ensure that once the handle object instance has been transferred, that instance cannot be transferred again. +1. A [[track]] internal slot that is a weak reference to the proxied {{MediaStreamTrack}} object. + +### Constructor ### {#mediastreamtrackhandle-constructor} + + MediaStreamTrackHandle(|track|) + +1. Let |handle| be a new {{MediaStreamTrackHandle}} object. +1. Set |handle|.[[transferable]] to true. +1. Set |handle|.[[track]] to a weak reference to |track|. +1. Return |handle|. + +The {{MediaStreamTrackHandle}} [=transfer steps=], given |value| and |dataHolder|, are: +1. If |value|.`[[transferable]]` is false, throw a {{DataCloneError|| DOMException. +1. Set |value|.`[[transferable]]` to false. +1. Set |dataHolder|.[[Track]] to |handle|.`[[track]]`. +1. Set |value|.`[[track]]` to null. + +The {{MediaStreamTrackHandle}} [=transfer-receiving steps=], given |dataHolder| and |handle|, are: +1. Set |handle|.`[[track]]` to |dataHolder|.[[Track]]. + +The {{MediaStreamTrackHandle}} can increase the lifetime of its proxied {{MediaStreamTrack}}, in particular: +1. A {{MediaStreamTrack}} MUST not be garbage collected if it is `live` and it is referenced by a {{MediaStreamTrackHandle}} via its `[[track]]` slot. +1. The proxied {{MediaStreamTrack}} MUST not be garbage collected when a referencing {{MediaStreamTrackHandle}} is being transferred. + +As soon as the {{MediaStreamTrackHandle}} is created, the user agent MUST proceed as if a `ended` event listener is registered on the proxied {{MediaStreamTrack}}. +When the {{MediaStreamTrackHandle}} is destroyed or its `[[track]]` slot is set to null, the user agent MUST proceed as if that `ended` event listener was no longer registered on the proxied {{MediaStreamTrack}}. ## VideoTrackGenerator ## {#video-track-generator} A {{VideoTrackGenerator}} allows the creation of a video source for a @@ -497,22 +541,22 @@ effects on video elements. const stream = await navigator.mediaDevices.getUserMedia({video:true}); const videoBefore = document.getElementById('video-before'); const videoAfter = document.getElementById('video-after'); -videoBefore.srcObject = stream.clone(); +videoBefore.srcObject = stream; -const [track] = stream.getVideoTracks(); +const trackHandle = new MediaStreamTrackHandle(stream.getVideoTracks()[0]); const worker = new Worker('worker.js'); -worker.postMessage({track}, [track]); +worker.postMessage({trackHandle}, [trackHandle]); const {data} = await new Promise(r => worker.onmessage); videoAfter.srcObject = new MediaStream([data.track]); // worker.js -self.onmessage = async ({data: {track}}) => { +self.onmessage = async ({data: {trackHandle}}) => { const source = new VideoTrackGenerator(); parent.postMessage({track: source.track}, [source.track]); - const {readable} = new MediaStreamTrackProcessor({track}); + const {readable} = new MediaStreamTrackProcessor({track: trackHandle}); const transformer = new TransformStream({ async transform(frame, controller) { const facePosition = await detectFace(frame); From 2d5d00dfefaea9781f62dab61a00e1b8ee75aa99 Mon Sep 17 00:00:00 2001 From: Youenn Fablet Date: Wed, 17 Dec 2025 18:07:23 +0100 Subject: [PATCH 2/6] fix style --- index.bs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/index.bs b/index.bs index 4b6757f..09ef4d1 100644 --- a/index.bs +++ b/index.bs @@ -167,9 +167,9 @@ application that have not yet been handled. 1. If |init|.{{MediaStreamTrackProcessorInit/track}} is a {{MediaStreamTrack}} that is not valid, throw a {{TypeError}}. 1. If |init|.{{MediaStreamTrackProcessorInit/track}} is a {{MediaStreamTrackHandle}}, run the following substeps: - 1. If |init|.{{MediaStreamTrackProcessorInit/track}}.[[transferable]] is false, throw a {{ TypeError}}. + 1. If |init|.{{MediaStreamTrackProcessorInit/track}}.`[[transferable]]` is false, throw a {{ TypeError}}. 1. If |init|.{{MediaStreamTrackProcessorInit/track}}.`[[track]]` is referencing a non valid {{MediaStreamTrack}}, throw a {{TypeError}}. - 1. Set |init|.{{MediaStreamTrackProcessorInit/track}}.[[transferable]] to false. + 1. Set |init|.{{MediaStreamTrackProcessorInit/track}}.`[[transferable]]` to false. 1. Let |maxBufferSize| be 1. 1. If |init|.{{MediaStreamTrackProcessorInit/maxBufferSize}} has an integer value greater than 1, run the following substeps: 1. Set |maxBufferSize| to |init|.{{MediaStreamTrackProcessorInit/maxBufferSize}}. @@ -252,31 +252,31 @@ When the `[[track]]` of a {{MediaStreamTrackProcessor}} |processor| [=track|ends=], the [=processorClose=] algorithm must be executed with |processor| as parameter. -### MediaStreamTrackHandle ### +### MediaStreamTrackHandle ### {#mediastreamtrackhandle} The {{MediaStreamTrackHandle}} interface represents a proxy to a {{MediaStreamTrack}} object that is useful for making a DedicatedWorkerGlobalScope {{MediaStreamTrackProcessor}} a sink to a {{MediaStreamTrack}} living in another context. A {{MediaStreamTrackHandle}} has the following slots: -1. A [[transferable]] internal slot that is used to ensure that once the handle object instance has been transferred, that instance cannot be transferred again. -1. A [[track]] internal slot that is a weak reference to the proxied {{MediaStreamTrack}} object. +1. A `[[transferable]]` internal slot that is used to ensure that once the handle object instance has been transferred, that instance cannot be transferred again. +1. A `[[track]]` internal slot that is a weak reference to the proxied {{MediaStreamTrack}} object. ### Constructor ### {#mediastreamtrackhandle-constructor} MediaStreamTrackHandle(|track|) 1. Let |handle| be a new {{MediaStreamTrackHandle}} object. -1. Set |handle|.[[transferable]] to true. -1. Set |handle|.[[track]] to a weak reference to |track|. +1. Set |handle|.`[[transferable]]` to true. +1. Set |handle|.`[[track]]` to a weak reference to |track|. 1. Return |handle|. The {{MediaStreamTrackHandle}} [=transfer steps=], given |value| and |dataHolder|, are: -1. If |value|.`[[transferable]]` is false, throw a {{DataCloneError|| DOMException. +1. If |value|.`[[transferable]]` is false, throw a {{DataCloneError}} DOMException. 1. Set |value|.`[[transferable]]` to false. -1. Set |dataHolder|.[[Track]] to |handle|.`[[track]]`. +1. Set |dataHolder|.`[[track]]` to |handle|.`[[track]]`. 1. Set |value|.`[[track]]` to null. The {{MediaStreamTrackHandle}} [=transfer-receiving steps=], given |dataHolder| and |handle|, are: -1. Set |handle|.`[[track]]` to |dataHolder|.[[Track]]. +1. Set |handle|.`[[track]]` to |dataHolder|.`[[track]]`. The {{MediaStreamTrackHandle}} can increase the lifetime of its proxied {{MediaStreamTrack}}, in particular: 1. A {{MediaStreamTrack}} MUST not be garbage collected if it is `live` and it is referenced by a {{MediaStreamTrackHandle}} via its `[[track]]` slot. From 171e98193ce1c17ea9e32e8030bf9002325aa328 Mon Sep 17 00:00:00 2001 From: Youenn Fablet Date: Thu, 18 Dec 2025 17:10:37 +0100 Subject: [PATCH 3/6] Update according feedback --- index.bs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/index.bs b/index.bs index 09ef4d1..1ce3d17 100644 --- a/index.bs +++ b/index.bs @@ -169,7 +169,6 @@ application that have not yet been handled. 1. If |init|.{{MediaStreamTrackProcessorInit/track}} is a {{MediaStreamTrackHandle}}, run the following substeps: 1. If |init|.{{MediaStreamTrackProcessorInit/track}}.`[[transferable]]` is false, throw a {{ TypeError}}. 1. If |init|.{{MediaStreamTrackProcessorInit/track}}.`[[track]]` is referencing a non valid {{MediaStreamTrack}}, throw a {{TypeError}}. - 1. Set |init|.{{MediaStreamTrackProcessorInit/track}}.`[[transferable]]` to false. 1. Let |maxBufferSize| be 1. 1. If |init|.{{MediaStreamTrackProcessorInit/maxBufferSize}} has an integer value greater than 1, run the following substeps: 1. Set |maxBufferSize| to |init|.{{MediaStreamTrackProcessorInit/maxBufferSize}}. @@ -211,13 +210,12 @@ The maybeReadFrame algorithm is given a |processor| as input. It is d The processorCancel algorithm is given a |processor| as input. It is defined by running the following steps: 1. Run the [=processorClose=] algorithm with |processor| as parameter. -3. Return [=a promise resolved with=] undefined. +1. Return [=a promise resolved with=] undefined. The processorClose algorithm is given a |processor| as input. It is defined by running the following steps: 1. If |processor|.`[[isClosed]]` is true, abort these steps. 1. Disconnect |processor| from |processor|.`[[track]]`. The mechanism to do this is UA specific and the result is that |processor| is no longer a sink of |processor|.`[[track]]`. -1. If |processor|.`[[track]]` is a {{MediaStreamTrackHandle}}, set |processor|.`[[track]]`.`[[track]` to null. 1. Set |processor|.`[[track]]` to null. 1. [$ReadableStreamDefaultControllerClose|Close$] |processor|.{{MediaStreamTrackProcessor/readable}}.[=ReadableStream/[[controller]]=]. 1. [=list/Empty=] |processor|.`[[queue]]`. @@ -278,12 +276,9 @@ The {{MediaStreamTrackHandle}} [=transfer steps=], given |value| and |dataHolder The {{MediaStreamTrackHandle}} [=transfer-receiving steps=], given |dataHolder| and |handle|, are: 1. Set |handle|.`[[track]]` to |dataHolder|.`[[track]]`. -The {{MediaStreamTrackHandle}} can increase the lifetime of its proxied {{MediaStreamTrack}}, in particular: -1. A {{MediaStreamTrack}} MUST not be garbage collected if it is `live` and it is referenced by a {{MediaStreamTrackHandle}} via its `[[track]]` slot. -1. The proxied {{MediaStreamTrack}} MUST not be garbage collected when a referencing {{MediaStreamTrackHandle}} is being transferred. - -As soon as the {{MediaStreamTrackHandle}} is created, the user agent MUST proceed as if a `ended` event listener is registered on the proxied {{MediaStreamTrack}}. -When the {{MediaStreamTrackHandle}} is destroyed or its `[[track]]` slot is set to null, the user agent MUST proceed as if that `ended` event listener was no longer registered on the proxied {{MediaStreamTrack}}. +The {{MediaStreamTrackHandle}} can increase the lifetime of its proxied {{MediaStreamTrack}}. +In particular, a {{MediaStreamTrack}} MUST not be garbage collected if it is `live` and it is referenced by a {{MediaStreamTrackHandle}} via its `[[track]]` slot. +Also, a {{MediaStreamTrack}} MUST not be garbage collected if it is referenced from a data holder that is being transferred. ## VideoTrackGenerator ## {#video-track-generator} A {{VideoTrackGenerator}} allows the creation of a video source for a From 648d408a29c79eb1e8495370214ab72b7751275f Mon Sep 17 00:00:00 2001 From: Youenn Fablet Date: Thu, 15 Jan 2026 14:40:49 +0100 Subject: [PATCH 4/6] Add minor clarifications --- index.bs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.bs b/index.bs index 1ce3d17..6602ef8 100644 --- a/index.bs +++ b/index.bs @@ -215,7 +215,7 @@ It is defined by running the following steps: The processorClose algorithm is given a |processor| as input. It is defined by running the following steps: 1. If |processor|.`[[isClosed]]` is true, abort these steps. -1. Disconnect |processor| from |processor|.`[[track]]`. The mechanism to do this is UA specific and the result is that |processor| is no longer a sink of |processor|.`[[track]]`. +1. Disconnect |processor| from |processor|.`[[track]]`. The mechanism to do this is UA specific and the result is that |processor| is no longer a sink of |processor|.`[[track]]`, or the {{MediaStreamTrack}} referenced by it. 1. Set |processor|.`[[track]]` to null. 1. [$ReadableStreamDefaultControllerClose|Close$] |processor|.{{MediaStreamTrackProcessor/readable}}.[=ReadableStream/[[controller]]=]. 1. [=list/Empty=] |processor|.`[[queue]]`. @@ -277,7 +277,7 @@ The {{MediaStreamTrackHandle}} [=transfer-receiving steps=], given |dataHolder| 1. Set |handle|.`[[track]]` to |dataHolder|.`[[track]]`. The {{MediaStreamTrackHandle}} can increase the lifetime of its proxied {{MediaStreamTrack}}. -In particular, a {{MediaStreamTrack}} MUST not be garbage collected if it is `live` and it is referenced by a {{MediaStreamTrackHandle}} via its `[[track]]` slot. +From a garbage collection point of view, it is as if a {{MediaStreamTrackHandle}} holds a strong reference to its {{MediaStreamTrack}}, as long as the referenced {{MediaStreamTrack}} is `live`. Also, a {{MediaStreamTrack}} MUST not be garbage collected if it is referenced from a data holder that is being transferred. ## VideoTrackGenerator ## {#video-track-generator} From 9efaffe9f04cbf75926a2439c581306b1df4a0ac Mon Sep 17 00:00:00 2001 From: youennf Date: Thu, 15 Jan 2026 16:41:59 +0100 Subject: [PATCH 5/6] Apply suggestion from @youennf --- index.bs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.bs b/index.bs index 6602ef8..b9daf47 100644 --- a/index.bs +++ b/index.bs @@ -264,7 +264,7 @@ A {{MediaStreamTrackHandle}} has the following slots: 1. Let |handle| be a new {{MediaStreamTrackHandle}} object. 1. Set |handle|.`[[transferable]]` to true. -1. Set |handle|.`[[track]]` to a weak reference to |track|. +1. Set |handle|.`[[track]]` to an internal reference to |track|. 1. Return |handle|. The {{MediaStreamTrackHandle}} [=transfer steps=], given |value| and |dataHolder|, are: From 4a6c133fabb77e39bf8d7d68fd2fa1ef6f88ecac Mon Sep 17 00:00:00 2001 From: youennf Date: Thu, 15 Jan 2026 16:49:24 +0100 Subject: [PATCH 6/6] Apply suggestion from @youennf, following jan-ivar's input --- index.bs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.bs b/index.bs index b9daf47..4b54fab 100644 --- a/index.bs +++ b/index.bs @@ -277,7 +277,7 @@ The {{MediaStreamTrackHandle}} [=transfer-receiving steps=], given |dataHolder| 1. Set |handle|.`[[track]]` to |dataHolder|.`[[track]]`. The {{MediaStreamTrackHandle}} can increase the lifetime of its proxied {{MediaStreamTrack}}. -From a garbage collection point of view, it is as if a {{MediaStreamTrackHandle}} holds a strong reference to its {{MediaStreamTrack}}, as long as the referenced {{MediaStreamTrack}} is `live`. +From a garbage collection point of view, it is as if a {{MediaStreamTrackHandle}} holds a strong reference to its {{MediaStreamTrack}}. Also, a {{MediaStreamTrack}} MUST not be garbage collected if it is referenced from a data holder that is being transferred. ## VideoTrackGenerator ## {#video-track-generator}