使用流的 MediaStreamTrack 可插入媒体处理

W3C 工作草案

关于此文档的更多详细信息
此版本:
https://www.w3.org/TR/2026/WD-mediacapture-transform-20260416/
最新发布版本:
https://www.w3.org/TR/mediacapture-transform/
编辑者草案:
https://w3c.github.io/mediacapture-transform/
以前版本:
历史:
https://www.w3.org/standards/history/mediacapture-transform/
反馈:
public-webrtc@w3.org 主题行为 “[mediacapture-transform] … 消息主题 …” (归档)
GitHub
编辑:
(Google)
(Google)

摘要

此 API 定义了一个 API 表面,用于操作携带原始数据的 MediaStreamTrack 的比特。

此文档的状态

本节描述了此文档在其发布时的状态。当前 W3C 出版物以及此技术报告最新修订版的列表 可在 W3C 技术报告 索引中找到。

此文档由 Web 实时通信工作组 作为工作草案使用 推荐标准 轨道发布。此文档旨在成为 W3C 推荐标准。

如果你希望对此文档发表评论,请将评论发送到 public-webrtc@w3.org订阅归档)。 发送电子邮件时, 请在主题中放入文本 “mediacapture-transform”, 最好像这样: “[mediacapture-transform] …评论摘要…”。 欢迎所有评论。

作为工作草案发布并不意味着 W3C 及其成员的认可。这是一份草案文档,可能会在任何时候被 更新、替换或 被其他文档废弃。除作为正在进行中的工作外,引用此 文档是不合适的。

此文档由一个根据 W3C 专利政策运作的小组产生。 W3C 维护一个任何专利披露的公开列表, 这些披露与该小组的交付成果有关; 该页面还包括披露专利的说明。 实际知晓某项专利且认为该专利包含必要权利要求的个人, 必须按照 W3C 专利政策第 6 节披露该信息。

此文档受 2025 年 8 月 18 日 W3C 流程文档管辖。

1. 引言

[WEBRTC-NV-USE-CASES] 文档描述了若干 只有通过访问媒体才能实现的功能(需求 N20-N22), 包括但不限于:

这些用例进一步要求处理能够在 worker 线程中完成(需求 N23-N24)。

此规范给出了一个基于 [WEBCODECS][STREAMS] 的接口, 以提供对这类功能的访问。

此规范提供对原始媒体的访问, 原始媒体是诸如摄像头、麦克风、屏幕捕获等媒体源的输出, 或编解码器解码器部分的输出,以及编解码器 解码器部分的输入。处理后的媒体可由任何 能接收 MediaStreamTrack 的目标消费,包括 HTML <video> 标签、 RTCPeerConnection、canvas 或 MediaRecorder。

此规范明确旨在支持以下用例:

注: 关于是否应支持音频 用例,WG 尚无共识。

注: WG 预期 Streams 规范将采纳 相关 解释文档中概述的解决方案,以解决当前 Streams 规范的一些问题。

2. 规范

此规范展示了 [MEDIACAPTURE-STREAMS] 的 IDL 扩展。 它定义了一些继承 MediaStreamTrack 接口的新对象,并且 可以从 MediaStreamTrack 构造。

此 API 由两个元素组成。一个是轨道接收器, 能够将来自轨道的未编码媒体帧暴露给 ReadableStream。 另一个则是它的反向形式:它提供一个以 媒体帧为输入的轨道源。

2.1. MediaStreamTrackProcessor

MediaStreamTrackProcessor 允许创建一个 ReadableStream, 该流可以暴露流经 给定 MediaStreamTrack 的媒体。 如果该 MediaStreamTrack 是视频轨道, 则由流暴露的块将是 VideoFrame 对象。

这使 MediaStreamTrackProcessor 实际上成为 MediaStream 模型中的接收器。

MediaStreamTrackProcessor 内部包含一个循环队列, 该队列允许缓冲由其所连接轨道传入的媒体帧。 这种缓冲允许 MediaStreamTrackProcessor 临时保存等待从其关联 ReadableStream 中读取的帧。 应用程序可以通过在 MediaStreamTrackProcessor 构造函数中提供的参数来影响队列的最大大小。然而, 队列的最大大小由 UA 决定并且可以动态改变, 但不会超过应用程序请求的大小。 如果应用程序未提供最大大小参数,则 UA 可自由 决定队列的最大大小。

当一个新帧到达 MediaStreamTrackProcessor 时, 如果队列已达到其最大大小, 最旧的帧将从队列中移除,新帧将被 添加到队列中。这意味着,对于最大大小为 1 的队列这一特殊情况, 如果存在已排队的帧,它将始终是 最新的那个。

UA 也可在任何时候自由地从队列中移除任何帧。UA 可以为了节省资源或在特定情况下提高性能而移除帧。 在这种情况下,[[numDiscardedFrames]] 必须相应递增。 在所有情况下,未被丢弃的帧都必须按照其到达 MediaStreamTrackProcessor 的顺序提供给 ReadableStream

MediaStreamTrackProcessor 只在已对流发出读取请求时, 才会向其关联的 ReadableStream 提供帧。其想法是避免流的内部缓冲, 因为该内部缓冲不能给 UA 足够的灵活性来选择缓冲策略。

鼓励作者在不再需要帧时立即对输出 VideoFrames 调用 close()。 底层媒体资源由 MediaStreamTrack 的 源拥有。 不释放它们(或等待垃圾回收)可能导致源停止发出新的 VideoFrames

2.1.1. 接口定义

[Exposed=(Window,DedicatedWorker), Transferable]
interface MediaStreamTrackHandle {
   constructor(MediaStreamTrack track);
};

[Exposed=DedicatedWorker]
interface MediaStreamTrackProcessor {
    constructor(MediaStreamTrackProcessorInit init);
    readonly attribute ReadableStream readable;
    readonly attribute unsigned long long discardedFrames;
    readonly attribute unsigned long long totalFrames;
};

typedef (MediaStreamTrack or MediaStreamTrackHandle) MediaStreamTrackOrHandle;
dictionary MediaStreamTrackProcessorInit {
  required MediaStreamTrackOrHandle track;
  [EnforceRange] unsigned short maxBufferSize;
};

注: WG 已达成共识,该接口应 暴露在 DedicatedWorker 上。 关于该接口是否不应暴露在 Window 上,WG 尚无共识。

注: WG 已达成共识,应存在从 kind 为 "video" 的 MediaStreamTrack 创建 MediaStreamTrackProcessor 的能力。 关于是否应支持从 kind 为 "audio" 的 MediaStreamTrack 创建 MediaStreamTrackProcessor, WG 尚无共识。

2.1.2. 内部槽

[[track]]
其原始数据将由 MediaStreamTrackProcessor 暴露的轨道。
[[maxBufferSize]]
MediaStreamTrackProcessor 缓冲的媒体帧最大数量, 如应用程序所指定。如果应用程序没有提供它, 它可以没有值。其最小有效值为 1。
[[queue]]
一个队列, 用于缓冲尚未由应用程序读取的媒体帧
[[numPendingReads]]
一个整数,其值表示应用程序已发出但尚未处理的 读取请求数量。
[[isClosed]]
一个布尔值,其值指示该 MediaStreamTrackProcessor 是否已关闭。
[[numDiscardedFrames]]
MediaStreamTrackProcessor 丢弃的帧数。
[[numTotalFrames]]
MediaStreamTrackProcessor 接收的帧数,包括被丢弃的帧。

2.1.3. 构造函数

MediaStreamTrackProcessor(init)
  1. 如果 init.track 是一个无效的 MediaStreamTrack, 则抛出 TypeError

  2. 如果 init.track 是一个 MediaStreamTrackHandle, 则运行以下子步骤:

    1. 如果 init.track.[[transferable]] 为 false,则抛出 TypeError

    2. 如果 init.track.[[track]] 正在引用一个无效的 MediaStreamTrack, 则抛出 TypeError

  3. maxBufferSize 为 1。

  4. 如果 init.maxBufferSize 的整数值大于 1,则运行以下子步骤:

    1. maxBufferSize 设置为 init.maxBufferSize

    2. 用户代理可以决定将 maxBufferSize 钳制为更低的值,但不得低于 1。

      钳制 maxBufferSize 对某些源(例如摄像头)可能很有用, 例如在它们在任意给定时间只能使用有限数量的 VideoFrames 的情况下。

  5. processor 为一个新的 MediaStreamTrackProcessor 对象。

  6. processor.[[track]] 设置为 init.track

  7. processor.[[maxBufferSize]] 设置为 maxBufferSize

  8. processor.[[queue]] 设置为空队列

  9. processor.[[numPendingReads]] 设置为 0。

  10. processor.[[isClosed]] 设置为 false。

  11. processor.[[numDiscardedFrames]] 设置为 0。

  12. processor.[[numTotalFrames]] 设置为 0。

  13. 返回 processor

2.1.4. 属性

readable, 类型为 ReadableStream,只读
允许读取由存储在 [[track]] 内部槽中的 MediaStreamTrackMediaStreamTrackHandle 递送的帧。此属性在第一次被调用时 按照以下步骤创建:
  1. this.readable 初始化为一个新的 ReadableStream

  2. 设置 this.readable, 将其 pullAlgorithm 设置为以 this 为参数的 processorPull, 将 cancelAlgorithm 设置为以 this 为参数的 processorCancel, 并将 highWaterMark 设置为 0。

processorPull 算法以 processor 作为输入。其定义为以下步骤:

  1. processor.[[numPendingReads]] 的值递增 1。

  2. 排队一个任务,以 processor 为参数运行 maybeReadFrame 算法。

  3. 返回一个 undefined 兑现的 promise。

maybeReadFrame 算法以 processor 作为输入。 其定义为以下步骤:

  1. 如果 processor.[[queue]], 则中止这些步骤。

  2. 如果 processor.[[numPendingReads]] 等于零,则中止这些步骤。

  3. frame 为从 processor.[[queue]] 出队一个帧媒体数据的结果。

  4. frame 入队processor.readable

  5. processor.[[numPendingReads]] 递减 1。

  6. 转到步骤 1。

processorCancel 算法以 processor 作为输入。 它通过运行以下步骤定义:

  1. processor 为参数运行 processorClose 算法。

  2. 返回一个 undefined 兑现的 promise。

processorClose 算法以 processor 作为输入。 它通过运行以下步骤定义:

  1. 如果 processor.[[isClosed]] 为 true,则中止这些步骤。

  2. processorprocessor.[[track]] 断开。 执行此操作的机制是 UA 特定的,其结果是 processor 不再是 processor.[[track]] 的接收器,或不再是其引用的 MediaStreamTrack 的接收器。

  3. processor.[[track]] 设置为 null。

  4. 关闭 processor.readable.[[controller]]

  5. 清空 processor.[[queue]]

  6. processor.[[isClosed]] 设置为 true。

discardedFrames, 类型为 unsigned long long,只读
返回 processor.[[numDiscardedFrames]] 的值。
totalFrames, 类型为 unsigned long long,只读
返回 processor.[[numTotalFrames]] 的值。

2.1.5. 处理与轨道的交互

MediaStreamTrackProcessor processor[[track]]processor 递送 帧时,UA 必须以 processor 为参数执行 handleNewFrame 算法。

handleNewFrame 算法以 processor 作为输入。 它通过运行以下步骤定义:

  1. 如果 processor.[[queue]]processor.[[maxBufferSize]] 个元素,则运行以下步骤:

    1. droppedFrame 为从 processor.[[queue]] 出队的结果。

    2. droppedFrame 运行 Close VideoFrame 算法。

    3. processor.[[numDiscardedFrames]] 递增 1。

  2. processor.[[numTotalFrames]] 递增 1。

  3. 将新的帧媒体数据入队processor.[[queue]]

  4. 排队一个任务,以 processor 为参数运行 maybeReadFrame 算法。

在任何时候,UA 都可以从 processor.[[queue]] 移除任何帧。 UA 可以决定从 processor.[[queue]] 移除帧,例如, 为了防止资源耗尽或在某些情况下提高性能。 在这种情况下,processor.[[numDiscardedFrames]] 必须相应递增。

应用程序可以通过注意帧的时间戳中存在间隙,或通过查看 discardedFrames 来检测帧已被丢弃。

MediaStreamTrackProcessor processor[[track]] 结束时, 必须以 processor 为参数执行 processorClose 算法。

2.1.6. MediaStreamTrackHandle

MediaStreamTrackHandle 接口表示到 MediaStreamTrack 对象的代理,该代理可用于使 DedicatedWorkerGlobalScope 中的 MediaStreamTrackProcessor 成为存在于另一个上下文中的 MediaStreamTrack 的接收器。

MediaStreamTrackHandle 具有以下槽:

  1. 一个 [[transferable]] 内部槽,用于确保一旦 handle 对象实例被转移, 该实例就不能再次被转移。

  2. 一个 [[track]] 内部槽,它是到被代理的 MediaStreamTrack 对象的弱引用。

2.1.7. 构造函数

MediaStreamTrackHandle(track)
  1. handle 为一个新的 MediaStreamTrackHandle 对象。

  2. handle.[[transferable]] 设置为 true。

  3. handle.[[track]] 设置为到 track 的内部引用。

  4. 返回 handle

给定 valuedataHolderMediaStreamTrackHandle转移步骤为:

  1. 如果 value.[[transferable]] 为 false,则抛出 DataCloneError DOMException。

  2. value.[[transferable]] 设置为 false。

  3. dataHolder.[[track]] 设置为 handle.[[track]]

  4. value.[[track]] 设置为 null。

给定 dataHolderhandleMediaStreamTrackHandle转移接收步骤为:

  1. handle.[[track]] 设置为 dataHolder.[[track]]

MediaStreamTrackHandle 可以延长其所代理 MediaStreamTrack 的生命周期。 从垃圾回收的角度看,就好像 MediaStreamTrackHandle 持有对其 MediaStreamTrack 的强引用。 此外,如果某个 MediaStreamTrack 被一个正在转移的数据持有者引用,则不得对其进行垃圾回收。

2.2. VideoTrackGenerator

VideoTrackGenerator 允许在 MediaStream 模型中为 MediaStreamTrack 创建一个视频源,该视频源从由 VideoFrame 对象组成的 Stream 生成其帧。它有两个 readonly 属性:一个 writable WritableStream 和一个 track MediaStreamTrack

VideoTrackGenerator 是其 writable 属性的底层接收器]。track 属性 是输出。可以在 track 属性上使用 clone 方法,创建连接到同一个 VideoTrackGenerator 的更多轨道。

WritableStream 接受 VideoFrame 对象。 当一个 VideoFrame 被写入 writable 时, 会自动调用该帧的 close() 方法,使其内部 资源不再可从 JavaScript 访问。

注: WG 已达成共识,应存在能够 生成 kind 为 "video" 的 MediaStreamTrack 的源。 关于是否应存在能够生成 kind 为 "audio" 的 MediaStreamTrack 的源,WG 尚无共识。

2.2.1. 接口定义

[Exposed=DedicatedWorker]
interface VideoTrackGenerator {
  constructor();
  readonly attribute WritableStream writable;
  attribute boolean muted;
  readonly attribute MediaStreamTrack track;
};

注: WG 已达成共识,该接口应 暴露在 DedicatedWorker 上。 关于它是否应暴露在 Window 上,WG 尚无共识。

2.2.2. 内部槽

[[track]]
此源输出的 MediaStreamTrack
[[isMuted]]
一个布尔值,其值指示此源以及它所源出的所有 MediaStreamTrack 当前是否被 muted

2.2.3. 构造函数

VideoTrackGenerator()
  1. generator 为一个新的 VideoTrackGenerator 对象。

  2. track 为一个新创建MediaStreamTrack, 其 source 设置为 generatortieSourceToContext 设置为 false

  3. generator.track 初始化为 track

  4. 返回 generator

2.2.4. 属性

writable, 类型为 WritableStream,只读
允许向 VideoTrackGenerator 写入视频帧。 当此属性 第一次被访问时,它必须按照以下步骤初始化:
  1. this.writable 初始化为一个新的 WritableStream

  2. 设置 this.writable, 将其 writeAlgorithm 设置为以 this 为参数的 writeFrame, 将 closeAlgorithm 设置为以 this 为参数的 closeWritable, 并将 abortAlgorithm 设置为以 this 为参数的 closeWritable

writeFrame 算法以 generatorframe 作为输入。它通过运行 以下步骤定义:

  1. 如果 frame 不是 VideoFrame 对象,则返回一个 TypeError 拒绝的 promise。

  2. 如果 frame[[Detached]] 内部槽的值为 true,则返回一个 TypeError 拒绝的 promise。

  3. 如果 generator.[[isMuted]] 为 false,则对于每个源自 generator 的活动轨道(命名为 track),运行以下步骤:

    1. clone 为以 frame 运行 Clone videoFrame 算法的结果。

    2. clone 发送到 track

  4. frame 运行 Close VideoFrame 算法。

  5. 返回一个 undefined 兑现的 promise。

closeWritable 算法以 generator 作为输入。 它通过运行以下步骤定义。

  1. 对于源自 generator 的每个轨道 t结束 t

  2. 返回一个 undefined 兑现的 promise。

当媒体数据发送到轨道时,UA 可以应用处理 (例如裁剪和缩小),以确保发送到轨道的媒体数据 满足轨道的约束。每个轨道可以根据其约束接收 不同版本的媒体数据。

muted, 类型为 boolean
VideoTrackGenerator 静音。 getter 步骤为返回 this.[[isMuted]]。给定值 newValue 的 setter 步骤如下:
  1. 如果 newValue 等于 this.[[isMuted]],则中止这些步骤。

  2. this.[[isMuted]] 设置为 newValue

  3. 除非在此事件循环运行中已经排队过一个任务,否则排队一个任务来运行以下步骤:

    1. settledValuethis.[[isMuted]]

    2. 对于由 this 源出的每个活动轨道, 排队一个任务,以将轨道的 muted 状态设置settledValue

track, 类型为 MediaStreamTrack,只读
MediaStreamTrack 输出。getter 步骤为返回 this.[[track]]

2.2.5. MediaStreamTrack 行为的特化

VideoTrackGenerator 充当一个或多个 MediaStreamTrack 的源。 本节增加了对源自 VideoTrackGeneratorMediaStreamTrack 如何表现的说明。
2.2.5.1. stop
stop 方法停止轨道。当源自 VideoTrackGenerator 的最后一个轨道结束时,该 VideoTrackGeneratorwritable 会被关闭
2.2.5.2. 可约束属性

为任何源自 VideoTrackGeneratorMediaStreamTrack 定义以下可约束属性:

属性名称 注释
width ConstrainULong 作为设置,这是轨道收到的最新帧的宽度, 以像素为单位。 作为能力,max 必须反映 VideoFrame 可以具有的最大宽度,min 必须反映 VideoFrame 可以具有的最小宽度。
height ConstrainULong 作为设置,这是轨道收到的最新帧的高度, 以像素为单位。 作为能力,max 必须反映 VideoFrame 可以具有的最大高度,min 必须反映 VideoFrame 可以具有的最小高度。
frameRate ConstrainDouble 作为设置,这是基于轨道最近收到的帧估算出的帧率。 作为能力,min 必须为零,并且 max 必须为系统支持的最大帧率。
aspectRatio ConstrainDouble 作为设置,这是轨道递送的最新帧的纵横比; 这是以像素为单位的宽度除以高度所得的 double, 四舍五入到小数点后第十位。作为能力, min 必须为一个 VideoFrame 支持的最小纵横比, max 必须为一个 VideoFrame 支持的最大纵横比。
resizeMode ConstrainDOMString 作为设置,此字符串应为 VideoResizeModeEnum 的成员之一。 值 "none" 表示 MediaStreamTrack 输出的帧是写入到 支撑该轨道的 writable 的帧的未修改版本, 无论存在任何约束。 值 "crop-and-scale" 表示 MediaStreamTrack 输出的帧可以是源帧的裁剪和/或 缩小版本, 具体基于轨道的 width、height 和 aspectRatio 约束的值。 作为能力,值 "none" 和 "crop-and-scale" 都必须存在。

应用于源自 VideoTrackGenerator 的视频 MediaStreamTrackapplyConstraints 方法支持上面定义的属性。 例如,它可用于调整帧大小或调整轨道的帧率。 请注意,这些约束不会影响写入到 VideoTrackGeneratorwritableVideoFrame 对象, 只会影响已应用约束的轨道的输出。 还请注意,由于 VideoTrackGenerator 原则上可以为受支持的可约束属性产生 具有任意设置的媒体数据, 对由 VideoTrackGenerator 支撑的轨道调用 applyConstraints 通常不会因 OverconstrainedError 而失败,除非给定约束超出由 getCapabilities 报告的系统支持范围。

2.2.5.3. 事件和属性
事件和属性的工作方式与任何 MediaStreamTrack 相同。 需要注意的是,如果 VideoTrackGeneratorwritable 流被关闭,则所有与之连接的活动 轨道都会结束,并在其上触发 ended 事件。

3. 示例

3.1. 视频处理

考虑一个面部识别函数 detectFace(videoFrame),它返回面部位置 (以某种格式),以及一个操作函数 blurBackground(videoFrame, facePosition), 它返回一个与给定 videoFrame 类似的新 VideoFrame,但将 非面部部分模糊化。该示例还展示了在 video 元素上应用效果前后的 视频。
// main.js

const stream = await navigator.mediaDevices.getUserMedia({video:true});
const videoBefore = document.getElementById('video-before');
const videoAfter = document.getElementById('video-after');
videoBefore.srcObject = stream;

const trackHandle = new MediaStreamTrackHandle(stream.getVideoTracks()[0]);
const worker = new Worker('worker.js');
worker.postMessage({trackHandle}, [trackHandle]);

const {data} = await new Promise(r => worker.onmessage);
videoAfter.srcObject = new MediaStream([data.track]);

// worker.js

self.onmessage = async ({data: {trackHandle}}) => {
  const source = new VideoTrackGenerator();
  parent.postMessage({track: source.track}, [source.track]);

  const {readable} = new MediaStreamTrackProcessor({track: trackHandle});
  const transformer = new TransformStream({
    async transform(frame, controller) {
      const facePosition = await detectFace(frame);
      const newFrame = blurBackground(frame, facePosition);
      frame.close();
      controller.enqueue(newFrame);
    }
  });
  await readable.pipeThrough(transformer).pipeTo(source.writable);
};

3.2. 带约束的多消费者后处理

一个常见用例是从馈送到视频会议的实时摄像头视频中移除背景, 同时显示结果的实时自我预览。即使由于带宽约束造成的反压导致 实际发送所用的帧率可能下降,也希望自我预览具有高帧率。 这可以通过对轨道克隆应用约束来实现,从而避免必须处理 两次。
// main.js

const stream = await navigator.mediaDevices.getUserMedia({video:true});
const [track] = stream.getVideoTracks();
const worker = new Worker('worker.js');
worker.postMessage({track}, [track]);

const {data} = await new Promise(r => worker.onmessage);
const selfView = document.getElementById('video-self');
selfView.srcObject = new MediaStream([data.track.clone()]); // 60 fps

await data.track.applyConstraints({width: 320, height: 200, frameRate: 30});
const pc = new RTCPeerConnection(config);
pc.addTrack(data.track); // 30 fps

// worker.js

self.onmessage = async ({data: {track}}) => {
  const source = new VideoTrackGenerator();
  parent.postMessage({track: source.track}, [source.track]);

  const {readable} = new MediaStreamTrackProcessor({track});
  const transformer = new TransformStream({transform: myRemoveBackgroundFromVideo});
  await readable.pipeThrough(transformer).pipeTo(source.writable);
};

3.3. worker 中带约束的多消费者后处理

在 worker 中通过 WebTransport 发送视频帧时,能够显示更高帧率的自我预览 也同样相关。这里可以使用上面的相同技术, 区别在于约束是在 worker 中应用到轨道克隆上的。
// main.js

const stream = await navigator.mediaDevices.getUserMedia({video:true});
const [track] = stream.getVideoTracks();
const worker = new Worker('worker.js');
worker.postMessage({track}, [track]);

const {data} = await new Promise(r => worker.onmessage);
const selfView = document.getElementById('video-self');
selfView.srcObject = new MediaStream([data.track]); // 60 fps

// worker.js

self.onmessage = async ({data: {track}}) => {
  const source = new VideoTrackGenerator();
  const sendTrack = source.track.clone();
  parent.postMessage({track: source.track}, [source.track]);

  await sendTrack.applyConstraints({width: 320, height: 200, frameRate: 30});

  const wt = new WebTransport("https://webtransport.org:8080/up");

  const {readable} = new MediaStreamTrackProcessor({track});
  const transformer = new TransformStream({transform: myRemoveBackgroundFromVideo});
  await readable.pipeThrough(transformer)
    .pipeThrough({writable: source.writable, readable: sendTrack.readable}),
    .pipeThrough(createMyEncodeVideoStream({
      codec: "vp8",
      width: 640,
      height: 480,
      bitrate: 1000000,
    }))
    .pipeThrough(new TransformStream({transform: mySerializer}));
    .pipeTo(wt.createUnidirectionalStream()); // 30 fps
};

上面的示例避免使用 tee() 函数来服务多个 消费者,因为它对实时流存在问题。

为简洁起见,该示例还过度简化了使用 WebCodecs 包装器 对视频帧进行编码并通过单个 WebTransport 流发送(会造成 队头阻塞)的过程。

4. 实现建议

本节为信息性内容。

4.1. 与多个消费者一起使用

存在一些用例,程序员可能希望单个帧流 被多个消费者消费。

示例包括背景模糊函数的结果既应显示在自我预览中, 又应使用 VideoEncoder 进行编码的情况。

对于两个消费者都消费未处理帧且不希望同步的情况, 实例化多个 MediaStreamTrackProcessor 对象是一种稳健的解决方案。

对于两个消费者都打算使用 VideoTrackGenerator 将处理步骤的结果转换为 MediaStreamTrack 的情况, 例如在将处理后的流馈送到 <video> 标签和 RTCPeerConnection 时, 将生成的 MediaStreamTrack 附加到多个接收器可能是最合适的机制。

对于下游处理接收帧而不是流的情况,可以根据需要克隆帧 并将其发送到下游处理;"clone" 是一种廉价操作。

当流是某些处理的输出,并且两个分支都需要 Stream 对象 以进行进一步处理时,需要一个从一个流产生两个流的函数。

然而,标准 tee() 操作在此上下文中 存在问题:

因此,只有在完全理解其影响时, 才应对包含媒体的 Streams 使用 tee()。相反,应使用 更适合用例的自定义元素来分割流。

注: Streams 规范中已提交了一些 issue, 其解决方案可能会影响本节:https://github.com/whatwg/streams/issues/1157, https://github.com/whatwg/streams/issues/1156, https://github.com/whatwg/streams/issues/401, https://github.com/whatwg/streams/issues/1186

5. 安全和隐私注意事项

此 API 定义了一个 MediaStreamTrack 源和一个 MediaStreamTrack 接收器。 该源(VideoTrackGenerator)的安全性和隐私性 依赖于同源策略。也就是说,VideoTrackGenerator 能以 MediaStreamTrack 形式提供的数据,在 VideoFrame 对象被构造并推入 VideoTrackGenerator 之前,必须对文档可见。 任何试图使用跨源数据创建 VideoFrame 对象的尝试都会失败。 因此,VideoTrackGenerator 不会引入任何新的 指纹识别表面。

此 API 引入的 MediaStreamTrack 接收器(MediaStreamTrackProcessor) 将与其他 MediaStreamTrack 接收器(如 WebRTC 对等连接和媒体元素)暴露的相同数据 从 MediaStreamTrack 暴露出来。MediaStreamTrackProcessor 的安全性和隐私性依赖于 MediaStreamTrackProcessor 所连接轨道的 MediaStreamTrack 源的安全性和隐私性。例如,摄像头、麦克风和屏幕捕获轨道 依赖通过权限对话框进行的显式使用授权(参见 [MEDIACAPTURE-STREAMS][SCREEN-CAPTURE]), 而元素捕获和 VideoTrackGenerator 依赖同源策略。

MediaStreamTrackProcessor 的一个潜在问题是资源耗尽。 例如,站点可能持有过多打开的 VideoFrame 对象, 并耗尽由 GPU 内存支持的系统范围帧池。UA 可以 通过限制站点可持有的池支持帧数量来缓解这一风险。 这可以通过减少缓冲帧的最大数量, 并在达到预算限制后拒绝向 readable 递送更多帧来实现。意外耗尽也通过在 VideoFrame 对象写入 VideoTrackGenerator 后自动关闭它们来缓解。

6. 与早期提案的向后兼容性

本节为信息性内容。

此接口以前的提案具有如下 API:

[Exposed=Window,DedicatedWorker]
interface MediaStreamTrackGenerator : MediaStreamTrack {
    constructor(MediaStreamTrackGeneratorInit init);
    attribute WritableStream writable;  // VideoFrame or AudioData
};

dictionary MediaStreamTrackGeneratorInit {
  required DOMString kind;
};
此接口的 MediaStreamTrack 生成器是 MediaStreamTrack 的实例, 而不是包含一个 MediaStreamTrack。

VideoTrackGenerator 可以像这样在 MediaStreamTrackGenerator 之上进行 shim:

// 未测试,不太可能按所写内容工作!
class VideoTrackGenerator {
  constructor() {
     this.innerGenerator = new MediaStreamTrackGenerator({kind: 'video'});
     this.writable = this.innerGenerator.writable;
     this.track = this.innerGenerator.clone();
  }
  // 缺失:用于设置 "muted" 属性的 shim。
};

关于以前提案的进一步说明,包括涉及音频处理的考虑事项, 可在此文档的较早版本中找到。

注: 当我们完成仓库移动后, 此处将放置一个指向 chrome-96 分支的链接。

符合性

文档 约定

符合性要求通过描述性断言 与 RFC 2119 术语的组合来表达。 此文档规范性部分中的关键词 “MUST”、“MUST NOT”、“REQUIRED”、“SHALL”、“SHALL NOT”、“SHOULD”、“SHOULD NOT”、“RECOMMENDED”、 “MAY” 和 “OPTIONAL” 应按 RFC 2119 中所述进行解释。 然而,为了可读性, 这些词在本规范中并不全部以大写字母出现。

本规范的所有文本均为规范性内容, 除非明确标记为非规范性的章节、示例和注释。[RFC2119]

本规范中的示例以 “for example” 一词引入, 或通过 class="example" 与规范性文本分隔开来, 如下所示:

这是一个信息性示例的例子。

信息性注释以 “Note” 一词开头, 并通过 class="note" 与规范性文本分隔开来, 如下所示:

注,这是一个信息性注释。

符合性 算法

作为算法一部分以祈使语气表述的要求 (例如 "strip any leading space characters" 或 "return false and abort these steps") 应按引入该算法时所使用的关键词 ("must"、"should"、"may" 等) 的含义进行解释。

以算法或具体步骤表述的符合性要求 可以用任何方式实现, 只要最终结果等效即可。 特别是,本规范中定义的算法 旨在易于理解, 并非旨在高性能。 鼓励实现者进行优化。

索引

由本 规范定义的术语

由引用 定义的术语

参考文献

规范性参考文献

[CSS-TEXT-4]
Elika Etemad; et al. CSS 文本模块第 4 级. 2024 年 5 月 29 日。WD。URL: https://www.w3.org/TR/css-text-4/
[HTML]
Anne van Kesteren; et al. HTML 标准. 现行标准。URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra 标准。现行标准。URL: https://infra.spec.whatwg.org/
[MEDIACAPTURE-STREAMS]
Cullen Jennings; et al. 媒体捕获和 流。2025 年 10 月 9 日。CRD。URL: https://www.w3.org/TR/mediacapture-streams/
[RFC2119]
S. Bradner. 用于 RFC 中以 指示要求级别的关键词。1997 年 3 月。最佳当前实践。URL: https://datatracker.ietf.org/doc/html/rfc2119
[STREAMS]
Adam Rice; et al. Streams 标准。活 标准。URL: https://streams.spec.whatwg.org/
[WEBCODECS]
Paul Adenot; Eugene Zemtsov. WebCodecs。2026 年 4 月 14 日。WD。URL: https://www.w3.org/TR/webcodecs/
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL 标准。活 标准。URL: https://webidl.spec.whatwg.org/
[WEBRTC]
Cullen Jennings; et al. WebRTC:浏览器中的实时通信。 2025 年 3 月 13 日。REC。URL: https://www.w3.org/TR/webrtc/

信息性参考文献

[SCREEN-CAPTURE]
Jan-Ivar Bruaroey; Elad Alon. 屏幕 捕获。2025 年 7 月 17 日。WD。URL: https://www.w3.org/TR/screen-capture/
[WEBRTC-NV-USE-CASES]
Bernard Aboba. WebRTC 扩展 用例。2023 年 12 月 14 日。DNOTE。URL: https://www.w3.org/TR/webrtc-nv-use-cases/
[WEBTRANSPORT]
Nidhi Jaju; Victor Vasiliev; Jan-Ivar Bruaroey. WebTransport。2026 年 3 月 25 日。WD。URL: https://www.w3.org/TR/webtransport/

IDL 索引

[Exposed=(Window,DedicatedWorker), Transferable]
interface MediaStreamTrackHandle {
   constructor(MediaStreamTrack track);
};

[Exposed=DedicatedWorker]
interface MediaStreamTrackProcessor {
    constructor(MediaStreamTrackProcessorInit init);
    readonly attribute ReadableStream readable;
    readonly attribute unsigned long long discardedFrames;
    readonly attribute unsigned long long totalFrames;
};

typedef (MediaStreamTrack or MediaStreamTrackHandle) MediaStreamTrackOrHandle;
dictionary MediaStreamTrackProcessorInit {
  required MediaStreamTrackOrHandle track;
  [EnforceRange] unsigned short maxBufferSize;
};

[Exposed=DedicatedWorker]
interface VideoTrackGenerator {
  constructor();
  readonly attribute WritableStream writable;
  attribute boolean muted;
  readonly attribute MediaStreamTrack track;
};