Skip to content

Commit 7f7d68b

Browse files
authored
Explainer for "transferring ownership streams"
As discussed at last WebRTC/WHATWG Streams meeting, we are converging on a potential solution for VideoFrame handling in streams. This explainer is the first step towards a more formal solution.
1 parent cf4508d commit 7f7d68b

File tree

1 file changed

+123
-0
lines changed

1 file changed

+123
-0
lines changed

streams-for-raw-video-explainer.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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

Comments
 (0)