|
| 1 | +# Transferring Ownership Streams Explained |
| 2 | + |
| 3 | + |
| 4 | +## Introduction |
| 5 | + |
| 6 | +The streams APIs provide a convenient way to build processing pipelines. |
| 7 | +Using streams with `VideoFrame` objects has known shortcomings, as illustrated by https://github.com/whatwg/streams/issues/1155 or https://github.com/whatwg/streams/issues/1185 for instance. |
| 8 | +This proposal addresses these shortcomings by improving streams support for chunks similar but not limited to WebCodec `VideoFrame`. |
| 9 | +It could also be useful for streams making use of `ArrayBuffer` or chunks that own `ArrayBuffer` like `RTCEncodedVideoChunk`. |
| 10 | + |
| 11 | +Streams APIs can create coupling between the processing units of the pipeline when chunks in the pipe are mutable: |
| 12 | +a processing unit A might pass a chunk O to another unit B through a stream (say a `WritableStream`) W. |
| 13 | +If A keeps a reference to O, it might mutate O while B is doing processing based on O. |
| 14 | + |
| 15 | +This is especially an issue with chunks like `VideoFrame` objects that need to be closed explicitly. |
| 16 | +Taking the previous example, if A decides to close a `VideoFrame` after passing it to W but before B gets it, B will receive a closed `VideoFrame`, which is probably considered a bug. |
| 17 | +If A does not close `VideoFrame`, it is the responsibility of the remaining of the pipeline to close it. |
| 18 | +There is a need to clearly identify who is owning these chunks and who is responsible to close these chunks at any point in time. |
| 19 | + |
| 20 | +The proposed solution is to transfer ownership of a chunk to the stream when it gets written or enqueueud to the stream. |
| 21 | +By doing so, the processing unit that enqueues/writes chunks will not be able to mutate chunks manipulated by the stream and is relieved of the lifetime management of these chunks. |
| 22 | +Conversely, processing units take ownership of chunks when they receive them from a stream. |
| 23 | + |
| 24 | +Transferring ownership should be opt-in. For that purpose, a new streams type, named 'transfer' in this document, would be added. |
| 25 | + |
| 26 | +## Example |
| 27 | + |
| 28 | +Below is an example of JavaScript that shows how this can be used. |
| 29 | +The example creates a processing pipe starting with a VideoFrame stream and applying two transforms, one for doing a processing like logging every 30 frame, and one for doing background blur. |
| 30 | + |
| 31 | +```worker.js javascript |
| 32 | +function doBackgroundBlurOnVideoFrames(videoFrameStream, doLogging) |
| 33 | +{ |
| 34 | + // JavaScript custom transform. |
| 35 | + let frameCount = 0; |
| 36 | + const frameCountTransform = new TransformStream({ |
| 37 | + transform: async (videoFrame, controller) => { |
| 38 | + try { |
| 39 | + // videoFrame is under the responsibility of the script and must be closed when no longer needed. |
| 40 | + controller.enqueue(videoFrame); |
| 41 | + // controller.enqueue was called, videoFrame is transferred. |
| 42 | + if (!(++frameCount % 30) && doLogging) |
| 43 | + doLogging(frameCount); |
| 44 | + } catch (e) { |
| 45 | + // In case of exception, let's make sure videoFrame is closed. This is a no-op if videoFrame was previously transferred. |
| 46 | + videoFrame.close(); |
| 47 | + // If exception is unrecoverable, let's error the pipe. |
| 48 | + controller.error(e); |
| 49 | + } |
| 50 | + }, |
| 51 | + readableType: 'transfer', |
| 52 | + writableType: 'transfer' |
| 53 | + }); |
| 54 | + // Native transform is of type 'transfer' |
| 55 | + const backgroundBlurTransform = new BackgroundBlurTransform(); |
| 56 | + |
| 57 | + return videoFrameStream.pipeThrough(backgroundBlurTransform) |
| 58 | + .pipeThrough(frameCountTransform); |
| 59 | +} |
| 60 | +``` |
| 61 | + |
| 62 | +## Goals |
| 63 | + |
| 64 | +* Permit `ReadableStream`, `WritableStream` and `TransformStream` objects to take ownership of chunks they manipulate. |
| 65 | +* Permit to build a safe and optimal video pipeline using `ReadableStream`, `WritableStream` and `TransformStream` objects that manipulate `VideoFrame` objects. |
| 66 | +* Permit both native and JavaScript-based streams of type 'transfer'. |
| 67 | +* Permit to optimize streams pipelines of transferable chunks like `ArrayBuffer`, `RTCEncodedVideoFrame` or `RTCEncodedAudioFrame`. |
| 68 | +* Permit to tee a `ReadableStream` of `VideoFrame` objects without tight coupling between the teed branches. |
| 69 | + |
| 70 | +## Non-goals |
| 71 | + |
| 72 | +* Add support for transferring and closing of arbitrary JavaScript chunks. |
| 73 | + |
| 74 | +## Use cases |
| 75 | + |
| 76 | +* Performing realtime transformations of `VideoFrame` objects, for instance taking a camera `MediaStreamTrack` and applying |
| 77 | + a background blur effect as a `TransformStream` on each `VideoFrame` of the `MediaStreamTrack`. |
| 78 | + |
| 79 | +## End-user benefit |
| 80 | + |
| 81 | +* `VideoFrame` needs specific management and be closed as quickly as possible, without relying on garbage collection. |
| 82 | + This is important to not create hangs/stutters in the processing pipeline. By building support for safe patterns |
| 83 | + directly in streams, this will allow web developers to optimize `VideoFrame` management, and allow user experience |
| 84 | + to be more consistent accross devices. |
| 85 | + |
| 86 | +## Principles |
| 87 | + |
| 88 | +The envisioned changes to the streams specification could look like the following: |
| 89 | +* Add a new 'transfer' value that can be passed to `ReadableStream` type, `WritableStream` type and `TransformStream` readableType/writableType. |
| 90 | + For streams that do not use the 'transfer' type, nothing changes. |
| 91 | +* Streams of the 'transfer' type can only manipulate chunks that are marked both as Transferable and Serializable. |
| 92 | +* If a chunk that is either not Transferable or not Serializable is enqueued or written, the chunk is ignored as if it was never enqueued/written. |
| 93 | +* If a Transferable and Serializable chunk is enqueueud/written in a 'transfer' type `ReadableStreamDefaultController`, `TransformStreamDefaultController` |
| 94 | + or `WritableStreamDefaultWriter`, create a transferred version of the chunk using StructuredSerializeWithTransfer/StructuredDeserializeWithTransfer. |
| 95 | + Proceed with the regular stream algorithm by using the transferred chunk instead of the chunk itself. |
| 96 | +* Introduce a WhatWG streams 'close-able' concept. A chunk that is 'close-able' defines closing steps. |
| 97 | + For instance `VideoFrame` closing steps could be defined using https://www.w3.org/TR/webcodecs/#close-videoframe. |
| 98 | + `ArrayBuffer` closing steps could be defined using https://tc39.es/ecma262/#sec-detacharraybuffer. |
| 99 | + The 'close-able' steps should be a no-op on a transferred chunk. |
| 100 | +* Execute the closing steps of a 'close-able' chunk for streams with the 'transfer' type when resetting the queue of `ReadableStreamDefaultController` |
| 101 | + or emptying `WritableStream`.[[writeRequests]] in case of abort/error. |
| 102 | +* When calling tee() on a `ReadableStream` of the 'transfer' type, call ReadableStream with cloneForBranch2 equal to true. |
| 103 | +* To solve https://github.com/whatwg/streams/issues/1186, tee() on a `ReadableStream` of the 'transfer' type can take a 'realtime' parameter. |
| 104 | + When the 'realtime' parameter is used, chunks will be dropped on the branch that consumes more slowly to keep buffering limited to one chunk. |
| 105 | + The closing steps should be called for any chunk that gets dropped in that situation. |
| 106 | + |
| 107 | +## Alternatives |
| 108 | + |
| 109 | +* It is difficult to emulate neutering/closing of chunks especially in case of teeing or aborting a stream. |
| 110 | +* As discussed in https://github.com/whatwg/streams/issues/1155, lifetime management of chunks could potentially be done at the source level. |
| 111 | + But this is difficult to make it work without introducing tight coupling between producers and consumers. |
| 112 | +* The main alternative would be to design a VideoFrame specific API outside of WhatWG streams, which is feasible, as examplified by WebCodecs API. |
| 113 | + |
| 114 | +## Future Work |
| 115 | + |
| 116 | +* Evaluate what to do when enqueuing/writing a chunk that is not Transferable or not Serializable. We might want to reject the related promise without erroring the stream. |
| 117 | +* Evaluate the usefulness of supporting Serializable but not Transferable chunks, we might just need to create a copy through serialization steps then explicitly call closeable steps on the chunk. |
| 118 | +* Evaluate the usefulness of supporting Transferable but not Serializable chunks, in particular in how to handle `ReadableStream` tee(). |
| 119 | + If `ReadableStream` tee() exposes a parameter to enable structured cloning, it might sometimes fail with such chunks and we could piggy back on this behavior. |
| 120 | +* Evaluate the usefulness of adding a `TransformStream` type to set readableType and writableType to the same value. |
| 121 | +* Envision extending this support for arbitrary JavaScript chunks, for both transferring and explicit closing. |
| 122 | +* Envision to introduce close-able concept in WebIDL. |
| 123 | +* We might want to mention that, if we use detach steps for `ArrayBuffer`, implementations can directly deallocate the corresponding memory. |
0 commit comments