@relaymessenger/sdk 0.3.6-staging.5 → 0.3.6-staging.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +511 -29
  2. package/dist/a2ui.d.ts +111 -0
  3. package/dist/a2ui.d.ts.map +1 -0
  4. package/dist/a2ui.js +137 -0
  5. package/dist/a2ui.js.map +1 -0
  6. package/dist/buttons.d.ts +7 -2
  7. package/dist/buttons.d.ts.map +1 -1
  8. package/dist/buttons.js +13 -11
  9. package/dist/buttons.js.map +1 -1
  10. package/dist/calls/call-room.d.ts +110 -0
  11. package/dist/calls/call-room.d.ts.map +1 -0
  12. package/dist/calls/call-room.js +616 -0
  13. package/dist/calls/call-room.js.map +1 -0
  14. package/dist/calls/engine-werift-video.d.ts +83 -0
  15. package/dist/calls/engine-werift-video.d.ts.map +1 -0
  16. package/dist/calls/engine-werift-video.js +607 -0
  17. package/dist/calls/engine-werift-video.js.map +1 -0
  18. package/dist/calls/engine-werift.d.ts +48 -0
  19. package/dist/calls/engine-werift.d.ts.map +1 -0
  20. package/dist/calls/engine-werift.js +462 -0
  21. package/dist/calls/engine-werift.js.map +1 -0
  22. package/dist/calls/index.d.ts +11 -0
  23. package/dist/calls/index.d.ts.map +1 -0
  24. package/dist/calls/index.js +11 -0
  25. package/dist/calls/index.js.map +1 -0
  26. package/dist/calls/transport.d.ts +466 -0
  27. package/dist/calls/transport.d.ts.map +1 -0
  28. package/dist/calls/transport.js +1409 -0
  29. package/dist/calls/transport.js.map +1 -0
  30. package/dist/calls/video-frame.d.ts +56 -0
  31. package/dist/calls/video-frame.d.ts.map +1 -0
  32. package/dist/calls/video-frame.js +245 -0
  33. package/dist/calls/video-frame.js.map +1 -0
  34. package/dist/calls/video-presets.d.ts +39 -0
  35. package/dist/calls/video-presets.d.ts.map +1 -0
  36. package/dist/calls/video-presets.js +40 -0
  37. package/dist/calls/video-presets.js.map +1 -0
  38. package/dist/calls/video-rtp.d.ts +51 -0
  39. package/dist/calls/video-rtp.d.ts.map +1 -0
  40. package/dist/calls/video-rtp.js +294 -0
  41. package/dist/calls/video-rtp.js.map +1 -0
  42. package/dist/calls/video.d.ts +195 -0
  43. package/dist/calls/video.d.ts.map +1 -0
  44. package/dist/calls/video.js +228 -0
  45. package/dist/calls/video.js.map +1 -0
  46. package/dist/client.d.ts +197 -8
  47. package/dist/client.d.ts.map +1 -1
  48. package/dist/client.js +437 -16
  49. package/dist/client.js.map +1 -1
  50. package/dist/errors.d.ts +11 -0
  51. package/dist/errors.d.ts.map +1 -1
  52. package/dist/errors.js +16 -0
  53. package/dist/errors.js.map +1 -1
  54. package/dist/index.d.ts +8 -3
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +8 -3
  57. package/dist/index.js.map +1 -1
  58. package/dist/links.d.ts +58 -0
  59. package/dist/links.d.ts.map +1 -0
  60. package/dist/links.js +127 -0
  61. package/dist/links.js.map +1 -0
  62. package/dist/operations.d.ts +97 -9
  63. package/dist/operations.d.ts.map +1 -1
  64. package/dist/operations.js +129 -10
  65. package/dist/operations.js.map +1 -1
  66. package/dist/payment.d.ts +67 -0
  67. package/dist/payment.d.ts.map +1 -0
  68. package/dist/payment.js +147 -0
  69. package/dist/payment.js.map +1 -0
  70. package/dist/reply-target.d.ts +26 -0
  71. package/dist/reply-target.d.ts.map +1 -0
  72. package/dist/reply-target.js +59 -0
  73. package/dist/reply-target.js.map +1 -0
  74. package/dist/selection.d.ts +46 -0
  75. package/dist/selection.d.ts.map +1 -0
  76. package/dist/selection.js +142 -0
  77. package/dist/selection.js.map +1 -0
  78. package/dist/types.d.ts +1075 -78
  79. package/dist/types.d.ts.map +1 -1
  80. package/dist/websocket.d.ts +6 -0
  81. package/dist/websocket.d.ts.map +1 -1
  82. package/dist/websocket.js +29 -10
  83. package/dist/websocket.js.map +1 -1
  84. package/package.json +38 -3
  85. package/dist/calls-room.d.ts +0 -50
  86. package/dist/calls-room.d.ts.map +0 -1
  87. package/dist/calls-room.js +0 -319
  88. package/dist/calls-room.js.map +0 -1
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Video for the werift engine: `node-webcodecs` 1.3.0 encodes and decodes
3
+ * (its static build: LGPL FFmpeg with openh264 and libvpx), `rtp-packet`
4
+ * packetizes, `video-rtp.ts` depacketizes, werift carries RTP and RTCP.
5
+ *
6
+ * Choices measured on 2026-09-22 (/tmp/video-spike-webcodecs, the spike that
7
+ * picked this engine), each kept here:
8
+ * - `NODE_WEBCODECS_FORCE=static` selects the bundled FFmpeg
9
+ * (node-webcodecs dist/native.js `load()`; the default order tries a
10
+ * dynamic build linked against system FFmpeg first).
11
+ * - `hardwareAcceleration: "prefer-software"` on encoder and decoder: on a
12
+ * Mac, VideoToolbox otherwise takes the stream.
13
+ * - H.264 is sent as constrained baseline, Annex-B; the codec string FFmpeg
14
+ * reports for H.264 is "MPEG4/ISO/AVC", so the codec is never matched by
15
+ * FFmpeg's name, only by the negotiated MIME type.
16
+ * - The H.264 level in the stream is OpenH264's own choice for the frame
17
+ * size, as in libwebrtc, whose OpenH264 encoder sets no level
18
+ * (modules/video_coding/codecs/h264/h264_encoder_impl.cc): measured on
19
+ * 2026-09-26, the SPS carries level 3.1 at 1280x720 and 4.0 at 1920x1080
20
+ * whatever level the codec string names. The string names level 4.0
21
+ * (`H264_ENCODER_CODEC`), the level of 1080p at 30 fps.
22
+ * - With no `videoEncoding`, the bitrate and framerate are LiveKit's preset
23
+ * for the frame size being sent (video-presets.ts); a `videoEncoding` field
24
+ * the caller sets wins over the preset's.
25
+ * - A keyframe every 1 s, and on every PLI or FIR from the SFU.
26
+ * - The send codec is the first codec of the SFU's answer (werift
27
+ * rtpSender.js:408 `this.codec = params.codecs[0]`).
28
+ * - The receive codec is learned from the payload type of the first RTP
29
+ * packet, looked up in the pulled m-line's codecs: the SFU's pull offer
30
+ * lists every codec (spike receiver.mjs).
31
+ * - werift sends no PLI on loss by itself: the receiver sends one when the
32
+ * stream starts, when a frame is dropped and on a decode error, then every
33
+ * 1 s until a keyframe decodes (spike receiver.mjs `pliTimer`).
34
+ */
35
+ import { MediaStreamTrack, type RTCRtpTransceiver } from "werift";
36
+ import type { RelayMediaStreamTrackLike } from "./transport.js";
37
+ import { type RelayVideoFactory, type RelayVideoReceiverLike, type RelayVideoReceiverStats, type RelayVideoSenderLike, type RelayVideoSenderStats, type TrackPublishOptions } from "./video.js";
38
+ import { VideoFrame, VideoRotation } from "./video-frame.js";
39
+ type WebCodecs = typeof import("node-webcodecs");
40
+ export declare const KEYFRAME_INTERVAL_MS = 1000;
41
+ export declare const KEYFRAME_REQUEST_INTERVAL_MS = 1000;
42
+ /**
43
+ * Constrained baseline (profile_idc 66, constraint_set0 and set1), level 4.0:
44
+ * ITU-T H.264 Table A-1 allows 8,192 macroblocks a frame and 245,760 a second
45
+ * at level 4.0, and 1920x1080 at 30 fps is 8,160 and 244,800.
46
+ */
47
+ export declare const H264_ENCODER_CODEC = "avc1.42e028";
48
+ /** Loads node-webcodecs once, on first video use, so audio-only calls never load FFmpeg. */
49
+ export declare const loadWebCodecs: () => Promise<WebCodecs>;
50
+ /**
51
+ * Raw frames in, H.264 or VP8 RTP out on one werift track. One sender lives
52
+ * for the whole call; a restart binds it to the new peer's transceiver.
53
+ */
54
+ export declare class WeriftVideoSender implements RelayVideoSenderLike {
55
+ #private;
56
+ readonly track: RelayMediaStreamTrackLike;
57
+ constructor(wc: WebCodecs, options?: TrackPublishOptions);
58
+ /**
59
+ * Offer the codecs this sender can encode, the preferred one first (werift
60
+ * fills an empty `transceiver.codecs` from the peer's config at
61
+ * `createOffer`, peerConnection.js:520-523, so setting it here wins).
62
+ */
63
+ bind(transceiver: unknown, peer: unknown): void;
64
+ setEnabled(enabled: boolean): void;
65
+ capture(frame: VideoFrame, timestampUs: bigint, rotation: VideoRotation): void;
66
+ stats(): RelayVideoSenderStats;
67
+ close(): void;
68
+ }
69
+ /**
70
+ * One pulled video m-line: RTP in, decoded I420 frames out. RTP is counted
71
+ * always; it is reordered, assembled and decoded only while active.
72
+ */
73
+ export declare class WeriftVideoReceiver implements RelayVideoReceiverLike {
74
+ #private;
75
+ onframe: RelayVideoReceiverLike["onframe"];
76
+ constructor(track: MediaStreamTrack, transceiver: RTCRtpTransceiver | undefined);
77
+ setActive(active: boolean): void;
78
+ stats(): RelayVideoReceiverStats;
79
+ stop(): void;
80
+ }
81
+ export declare const createWeriftVideoFactory: () => RelayVideoFactory;
82
+ export {};
83
+ //# sourceMappingURL=engine-werift-video.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine-werift-video.d.ts","sourceRoot":"","sources":["../../src/calls/engine-werift-video.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,OAAO,EACL,gBAAgB,EAEhB,KAAK,iBAAiB,EAGvB,MAAM,QAAQ,CAAC;AAEhB,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EAIzB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAmB,UAAU,EAAE,aAAa,EAAoB,MAAM,kBAAkB,CAAC;AAWhG,KAAK,SAAS,GAAG,cAAc,gBAAgB,CAAC,CAAC;AAMjD,eAAO,MAAM,oBAAoB,OAAQ,CAAC;AAC1C,eAAO,MAAM,4BAA4B,OAAQ,CAAC;AAClD;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,gBAAgB,CAAC;AAUhD,4FAA4F;AAC5F,eAAO,MAAM,aAAa,QAAO,OAAO,CAAC,SAAS,CAgBjD,CAAC;AA0CF;;;GAGG;AACH,qBAAa,iBAAkB,YAAW,oBAAoB;;IAC5D,QAAQ,CAAC,KAAK,EAAE,yBAAyB,CAAC;gBA6B9B,EAAE,EAAE,SAAS,EAAE,OAAO,GAAE,mBAAwB;IAQ5D;;;;OAIG;IACH,IAAI,CAAC,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI;IAqB/C,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAKlC,OAAO,CAAC,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG,IAAI;IAqD9E,KAAK,IAAI,qBAAqB;IAmB9B,KAAK,IAAI,IAAI;CAyEd;AAED;;;GAGG;AACH,qBAAa,mBAAoB,YAAW,sBAAsB;;IAChE,OAAO,EAAE,sBAAsB,CAAC,SAAS,CAAC,CAAQ;gBA+BtC,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,iBAAiB,GAAG,SAAS;IAW/E,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IAehC,KAAK,IAAI,uBAAuB;IAiBhC,IAAI,IAAI,IAAI;CA8Kb;AAED,eAAO,MAAM,wBAAwB,QAAO,iBAI1C,CAAC"}
@@ -0,0 +1,607 @@
1
+ /**
2
+ * Video for the werift engine: `node-webcodecs` 1.3.0 encodes and decodes
3
+ * (its static build: LGPL FFmpeg with openh264 and libvpx), `rtp-packet`
4
+ * packetizes, `video-rtp.ts` depacketizes, werift carries RTP and RTCP.
5
+ *
6
+ * Choices measured on 2026-09-22 (/tmp/video-spike-webcodecs, the spike that
7
+ * picked this engine), each kept here:
8
+ * - `NODE_WEBCODECS_FORCE=static` selects the bundled FFmpeg
9
+ * (node-webcodecs dist/native.js `load()`; the default order tries a
10
+ * dynamic build linked against system FFmpeg first).
11
+ * - `hardwareAcceleration: "prefer-software"` on encoder and decoder: on a
12
+ * Mac, VideoToolbox otherwise takes the stream.
13
+ * - H.264 is sent as constrained baseline, Annex-B; the codec string FFmpeg
14
+ * reports for H.264 is "MPEG4/ISO/AVC", so the codec is never matched by
15
+ * FFmpeg's name, only by the negotiated MIME type.
16
+ * - The H.264 level in the stream is OpenH264's own choice for the frame
17
+ * size, as in libwebrtc, whose OpenH264 encoder sets no level
18
+ * (modules/video_coding/codecs/h264/h264_encoder_impl.cc): measured on
19
+ * 2026-09-26, the SPS carries level 3.1 at 1280x720 and 4.0 at 1920x1080
20
+ * whatever level the codec string names. The string names level 4.0
21
+ * (`H264_ENCODER_CODEC`), the level of 1080p at 30 fps.
22
+ * - With no `videoEncoding`, the bitrate and framerate are LiveKit's preset
23
+ * for the frame size being sent (video-presets.ts); a `videoEncoding` field
24
+ * the caller sets wins over the preset's.
25
+ * - A keyframe every 1 s, and on every PLI or FIR from the SFU.
26
+ * - The send codec is the first codec of the SFU's answer (werift
27
+ * rtpSender.js:408 `this.codec = params.codecs[0]`).
28
+ * - The receive codec is learned from the payload type of the first RTP
29
+ * packet, looked up in the pulled m-line's codecs: the SFU's pull offer
30
+ * lists every codec (spike receiver.mjs).
31
+ * - werift sends no PLI on loss by itself: the receiver sends one when the
32
+ * stream starts, when a frame is dropped and on a decode error, then every
33
+ * 1 s until a keyframe decodes (spike receiver.mjs `pliTimer`).
34
+ */
35
+ import { MediaStreamTrack, RtpPacket, } from "werift";
36
+ import { JitterBuffer } from "rtp-packet";
37
+ import { VideoCodec, } from "./video.js";
38
+ import { defaultVideoEncoding } from "./video-presets.js";
39
+ import { VideoBufferType, VideoFrame, VideoRotation, videoFrameLength } from "./video-frame.js";
40
+ import { VIDEO_CLOCK_RATE, VideoFrameAssembler, createVideoPacketizer, } from "./video-rtp.js";
41
+ export const KEYFRAME_INTERVAL_MS = 1_000;
42
+ export const KEYFRAME_REQUEST_INTERVAL_MS = 1_000;
43
+ /**
44
+ * Constrained baseline (profile_idc 66, constraint_set0 and set1), level 4.0:
45
+ * ITU-T H.264 Table A-1 allows 8,192 macroblocks a frame and 245,760 a second
46
+ * at level 4.0, and 1920x1080 at 30 fps is 8,160 and 244,800.
47
+ */
48
+ export const H264_ENCODER_CODEC = "avc1.42e028";
49
+ /** Frames waiting in the encoder before new captures are dropped instead of queued. */
50
+ const MAX_ENCODE_QUEUE = 3;
51
+ /** Reorder window ahead of the frame assembler (rtp-packet `JitterBuffer` default). */
52
+ const JITTER_LATENCY_MS = 50;
53
+ const STATS_WINDOW_MS = 5_000;
54
+ const CVO_URI = "urn:3gpp:video-orientation";
55
+ let webCodecs;
56
+ /** Loads node-webcodecs once, on first video use, so audio-only calls never load FFmpeg. */
57
+ export const loadWebCodecs = () => {
58
+ webCodecs ??= (async () => {
59
+ process.env.NODE_WEBCODECS_FORCE ??= "static";
60
+ try {
61
+ const module = await import("node-webcodecs");
62
+ if (!module.isNativeAvailable())
63
+ throw new Error("native binding did not load");
64
+ return module;
65
+ }
66
+ catch (error) {
67
+ webCodecs = undefined;
68
+ const reason = error instanceof Error ? error.message : String(error);
69
+ throw new Error(`Relay video needs the optional dependency node-webcodecs (prebuilt for darwin-arm64, linux-x64 and linux-arm64): ${reason}`);
70
+ }
71
+ })();
72
+ return webCodecs;
73
+ };
74
+ class FrameClock {
75
+ count = 0;
76
+ firstAt;
77
+ lastAt;
78
+ #recent = [];
79
+ mark(now = Date.now()) {
80
+ this.count += 1;
81
+ this.firstAt ??= now;
82
+ this.lastAt = now;
83
+ this.#recent.push(now);
84
+ this.recent(now);
85
+ }
86
+ recent(now = Date.now()) {
87
+ const floor = now - STATS_WINDOW_MS;
88
+ let drop = 0;
89
+ while (drop < this.#recent.length && this.#recent[drop] < floor)
90
+ drop += 1;
91
+ if (drop)
92
+ this.#recent.splice(0, drop);
93
+ return this.#recent.length;
94
+ }
95
+ }
96
+ const codecOf = (mimeType) => {
97
+ const lower = mimeType?.toLowerCase();
98
+ if (lower === "video/h264")
99
+ return "h264";
100
+ if (lower === "video/vp8")
101
+ return "vp8";
102
+ return undefined;
103
+ };
104
+ /** WebCodecs pixel format for a LiveKit buffer type the encoder takes directly. */
105
+ const encoderFormat = (type) => {
106
+ if (type === VideoBufferType.I420)
107
+ return "I420";
108
+ if (type === VideoBufferType.RGBA)
109
+ return "RGBA";
110
+ if (type === VideoBufferType.BGRA)
111
+ return "BGRA";
112
+ return undefined;
113
+ };
114
+ const monotonicUs = () => Math.round(performance.now() * 1_000);
115
+ /**
116
+ * Raw frames in, H.264 or VP8 RTP out on one werift track. One sender lives
117
+ * for the whole call; a restart binds it to the new peer's transceiver.
118
+ */
119
+ export class WeriftVideoSender {
120
+ track;
121
+ #track;
122
+ #wc;
123
+ #encoding;
124
+ #preferred;
125
+ #ssrc = (Math.random() * 0xffffffff) >>> 0;
126
+ #transceiver;
127
+ #unsubscribe = [];
128
+ #encoder;
129
+ #encoderKey = "";
130
+ #packetizer;
131
+ #packetizerCodec;
132
+ #enabled = true;
133
+ #closed = false;
134
+ #keyframeWanted = true;
135
+ #lastKeyframeAt = Number.NEGATIVE_INFINITY;
136
+ #lastTimestampUs = 0;
137
+ #codec;
138
+ #width;
139
+ #height;
140
+ #captured = 0;
141
+ #dropped = 0;
142
+ #keyframes = 0;
143
+ #rtpPackets = 0;
144
+ #bytes = 0;
145
+ #keyframeRequests = 0;
146
+ #encodeErrors = 0;
147
+ #encoded = new FrameClock();
148
+ constructor(wc, options = {}) {
149
+ this.#wc = wc;
150
+ this.#encoding = options.videoEncoding;
151
+ this.#preferred = options.videoCodec === VideoCodec.VP8 ? "vp8" : "h264";
152
+ this.#track = new MediaStreamTrack({ kind: "video", id: "camera", streamId: "relay-call" });
153
+ this.track = this.#track;
154
+ }
155
+ /**
156
+ * Offer the codecs this sender can encode, the preferred one first (werift
157
+ * fills an empty `transceiver.codecs` from the peer's config at
158
+ * `createOffer`, peerConnection.js:520-523, so setting it here wins).
159
+ */
160
+ bind(transceiver, peer) {
161
+ for (const unsubscribe of this.#unsubscribe)
162
+ unsubscribe();
163
+ this.#unsubscribe = [];
164
+ const bound = transceiver;
165
+ const rank = (mimeType) => (codecOf(mimeType) === this.#preferred ? 0 : 1);
166
+ bound.codecs = (peer.config.codecs.video ?? [])
167
+ .filter((codec) => codecOf(codec.mimeType) !== undefined)
168
+ .sort((a, b) => rank(a.mimeType) - rank(b.mimeType));
169
+ this.#transceiver = bound;
170
+ this.#keyframeWanted = true;
171
+ this.#packetizer = undefined;
172
+ this.#packetizerCodec = undefined;
173
+ const pli = bound.sender.onPictureLossIndication.subscribe(() => this.#requestKeyframe());
174
+ const rtcp = bound.sender.onRtcp.subscribe((packet) => {
175
+ // Payload-specific feedback (206) with FMT 4 is a Full Intra Request (RFC 5104 section 4.3.1).
176
+ const feedback = packet.feedback;
177
+ if (packet.type === 206 && feedback?.count === 4)
178
+ this.#requestKeyframe();
179
+ });
180
+ this.#unsubscribe.push(pli.unSubscribe, rtcp.unSubscribe);
181
+ }
182
+ setEnabled(enabled) {
183
+ if (enabled && !this.#enabled)
184
+ this.#keyframeWanted = true;
185
+ this.#enabled = enabled;
186
+ }
187
+ capture(frame, timestampUs, rotation) {
188
+ this.#captured += 1;
189
+ if (this.#closed || !this.#enabled) {
190
+ this.#dropped += 1;
191
+ return;
192
+ }
193
+ if (rotation !== VideoRotation.VIDEO_ROTATION_0) {
194
+ throw new Error("Relay video sends upright frames only; rotate the frame before captureFrame.");
195
+ }
196
+ // The first codec of the answer; undefined until the SFU has answered.
197
+ const codec = codecOf(this.#transceiver?.sender.codec?.mimeType);
198
+ if (!codec) {
199
+ this.#dropped += 1;
200
+ return;
201
+ }
202
+ this.#codec = codec;
203
+ let input = frame;
204
+ let format = encoderFormat(frame.type);
205
+ if (!format) {
206
+ input = frame.convert(VideoBufferType.I420);
207
+ format = "I420";
208
+ }
209
+ const expected = videoFrameLength(input.type, input.width, input.height);
210
+ if (input.data.length < expected) {
211
+ throw new Error(`VideoFrame data holds ${input.data.length} bytes; ${input.width}x${input.height} needs ${expected}.`);
212
+ }
213
+ const encoder = this.#ensureEncoder(codec, input.width, input.height);
214
+ if (encoder.encodeQueueSize >= MAX_ENCODE_QUEUE) {
215
+ this.#dropped += 1;
216
+ return;
217
+ }
218
+ let us = timestampUs === BigInt(0) ? monotonicUs() : Number(timestampUs);
219
+ if (us <= this.#lastTimestampUs)
220
+ us = this.#lastTimestampUs + 1;
221
+ this.#lastTimestampUs = us;
222
+ const now = Date.now();
223
+ const keyFrame = this.#keyframeWanted || now - this.#lastKeyframeAt >= KEYFRAME_INTERVAL_MS;
224
+ if (keyFrame) {
225
+ this.#keyframeWanted = false;
226
+ this.#lastKeyframeAt = now;
227
+ }
228
+ const wcFrame = new this.#wc.VideoFrame(input.data.subarray(0, expected), {
229
+ format,
230
+ codedWidth: input.width,
231
+ codedHeight: input.height,
232
+ timestamp: us,
233
+ });
234
+ try {
235
+ encoder.encode(wcFrame, { keyFrame });
236
+ }
237
+ finally {
238
+ wcFrame.close();
239
+ }
240
+ }
241
+ stats() {
242
+ return {
243
+ codec: this.#codec,
244
+ framesCaptured: this.#captured,
245
+ framesEncoded: this.#encoded.count,
246
+ framesDropped: this.#dropped,
247
+ keyframes: this.#keyframes,
248
+ rtpPackets: this.#rtpPackets,
249
+ bytes: this.#bytes,
250
+ keyframeRequests: this.#keyframeRequests,
251
+ encodeErrors: this.#encodeErrors,
252
+ firstFrameAt: this.#encoded.firstAt,
253
+ lastFrameAt: this.#encoded.lastAt,
254
+ recentFrames: this.#encoded.recent(),
255
+ width: this.#width,
256
+ height: this.#height,
257
+ };
258
+ }
259
+ close() {
260
+ if (this.#closed)
261
+ return;
262
+ this.#closed = true;
263
+ for (const unsubscribe of this.#unsubscribe)
264
+ unsubscribe();
265
+ this.#unsubscribe = [];
266
+ this.#closeEncoder();
267
+ this.#track.stop();
268
+ }
269
+ #requestKeyframe() {
270
+ this.#keyframeRequests += 1;
271
+ this.#keyframeWanted = true;
272
+ }
273
+ #ensureEncoder(codec, width, height) {
274
+ const key = `${codec}:${width}x${height}`;
275
+ if (this.#encoder && this.#encoderKey === key && this.#encoder.state === "configured")
276
+ return this.#encoder;
277
+ this.#closeEncoder();
278
+ const encoder = new this.#wc.VideoEncoder({
279
+ output: (chunk) => this.#output(codec, chunk),
280
+ error: () => {
281
+ this.#encodeErrors += 1;
282
+ this.#keyframeWanted = true;
283
+ if (this.#encoder === encoder)
284
+ this.#encoder = undefined;
285
+ },
286
+ });
287
+ const preset = defaultVideoEncoding(width, height);
288
+ encoder.configure({
289
+ codec: codec === "h264" ? H264_ENCODER_CODEC : "vp8",
290
+ width,
291
+ height,
292
+ bitrate: Number(this.#encoding?.maxBitrate ?? preset.maxBitrate),
293
+ framerate: this.#encoding?.maxFramerate ?? preset.maxFramerate,
294
+ latencyMode: "realtime",
295
+ hardwareAcceleration: "prefer-software",
296
+ ...(codec === "h264" ? { avc: { format: "annexb" } } : {}),
297
+ });
298
+ this.#encoder = encoder;
299
+ this.#encoderKey = key;
300
+ this.#width = width;
301
+ this.#height = height;
302
+ this.#keyframeWanted = true;
303
+ return encoder;
304
+ }
305
+ #closeEncoder() {
306
+ const encoder = this.#encoder;
307
+ this.#encoder = undefined;
308
+ this.#encoderKey = "";
309
+ if (!encoder || encoder.state === "closed")
310
+ return;
311
+ try {
312
+ encoder.close();
313
+ }
314
+ catch { /* already closed */ }
315
+ }
316
+ #output(codec, chunk) {
317
+ if (this.#closed)
318
+ return;
319
+ const data = new Uint8Array(chunk.byteLength);
320
+ chunk.copyTo(data);
321
+ if (this.#packetizerCodec !== codec || !this.#packetizer) {
322
+ // werift's sender rewrites SSRC, payload type, sequence and timestamp
323
+ // base (rtpSender.js:544-549), so these are local placeholders.
324
+ this.#packetizer = createVideoPacketizer(codec, { ssrc: this.#ssrc, payloadType: 96 });
325
+ this.#packetizerCodec = codec;
326
+ }
327
+ const keyframe = chunk.type === "key";
328
+ const packets = this.#packetizer.packetize(data, chunk.timestamp, keyframe);
329
+ for (const packet of packets) {
330
+ this.#track.writeRtp(RtpPacket.deSerialize(packet));
331
+ this.#rtpPackets += 1;
332
+ this.#bytes += packet.length;
333
+ }
334
+ if (keyframe)
335
+ this.#keyframes += 1;
336
+ this.#encoded.mark();
337
+ }
338
+ }
339
+ /**
340
+ * One pulled video m-line: RTP in, decoded I420 frames out. RTP is counted
341
+ * always; it is reordered, assembled and decoded only while active.
342
+ */
343
+ export class WeriftVideoReceiver {
344
+ onframe = null;
345
+ #track;
346
+ #transceiver;
347
+ #unsubscribe;
348
+ #wc;
349
+ #active = false;
350
+ #stopped = false;
351
+ #payloadType;
352
+ #codec;
353
+ #codecLabel;
354
+ #codecString = "";
355
+ #ssrc;
356
+ #jitter;
357
+ #assembler;
358
+ #decoder;
359
+ #waitingForKeyframe = true;
360
+ #lastKeyframeRequestAt = Number.NEGATIVE_INFINITY;
361
+ #keyframeTimer;
362
+ #rotation = VideoRotation.VIDEO_ROTATION_0;
363
+ #firstRtpTimestamp;
364
+ #lastRtpTimestamp = 0;
365
+ #rtpTimestampWraps = 0;
366
+ #outputChain = Promise.resolve();
367
+ #rtpPackets = 0;
368
+ #skippedDeltas = 0;
369
+ #decodeErrors = 0;
370
+ #keyframeRequests = 0;
371
+ #width;
372
+ #height;
373
+ #decoded = new FrameClock();
374
+ constructor(track, transceiver) {
375
+ this.#track = track;
376
+ this.#transceiver = transceiver;
377
+ const { unSubscribe } = track.onReceiveRtp.subscribe((rtp, extensions) => {
378
+ this.#rtpPackets += 1;
379
+ if (!this.#active || this.#stopped)
380
+ return;
381
+ this.#receive(rtp, extensions);
382
+ });
383
+ this.#unsubscribe = unSubscribe;
384
+ }
385
+ setActive(active) {
386
+ if (this.#stopped || active === this.#active)
387
+ return;
388
+ this.#active = active;
389
+ if (!active) {
390
+ this.#resetPipeline();
391
+ return;
392
+ }
393
+ void loadWebCodecs().then((wc) => {
394
+ this.#wc = wc;
395
+ if (this.#active)
396
+ this.#requestKeyframe();
397
+ }, () => {
398
+ this.#decodeErrors += 1;
399
+ });
400
+ }
401
+ stats() {
402
+ return {
403
+ codec: this.#codecLabel,
404
+ rtpPackets: this.#rtpPackets,
405
+ framesAssembled: this.#assembler?.frames ?? 0,
406
+ framesDropped: (this.#assembler?.dropped ?? 0) + this.#skippedDeltas,
407
+ framesDecoded: this.#decoded.count,
408
+ decodeErrors: this.#decodeErrors,
409
+ keyframeRequests: this.#keyframeRequests,
410
+ firstFrameAt: this.#decoded.firstAt,
411
+ lastFrameAt: this.#decoded.lastAt,
412
+ recentFrames: this.#decoded.recent(),
413
+ width: this.#width,
414
+ height: this.#height,
415
+ };
416
+ }
417
+ stop() {
418
+ if (this.#stopped)
419
+ return;
420
+ this.#stopped = true;
421
+ this.onframe = null;
422
+ this.#unsubscribe();
423
+ this.#resetPipeline();
424
+ }
425
+ #receive(rtp, extensions) {
426
+ const header = rtp.header;
427
+ if (this.#payloadType === undefined) {
428
+ this.#payloadType = header.payloadType;
429
+ const codec = this.#transceiver?.codecs.find((candidate) => candidate.payloadType === header.payloadType);
430
+ this.#codecLabel = codec?.mimeType ?? `pt ${header.payloadType}`;
431
+ this.#codec = codecOf(codec?.mimeType);
432
+ const profile = /profile-level-id=([0-9a-f]{6})/i.exec(codec?.parameters ?? "")?.[1];
433
+ this.#codecString = this.#codec === "vp8" ? "vp8" : `avc1.${(profile ?? "42e01f").toLowerCase()}`;
434
+ }
435
+ if (header.payloadType !== this.#payloadType || !this.#codec || !rtp.payload?.length)
436
+ return;
437
+ this.#ssrc = header.ssrc;
438
+ const orientation = extensions?.[CVO_URI];
439
+ if (orientation)
440
+ this.#rotation = (((orientation.r1 ?? 0) << 1) | (orientation.r0 ?? 0));
441
+ if (!this.#jitter) {
442
+ const codec = this.#codec;
443
+ this.#assembler = new VideoFrameAssembler(codec, (frame) => this.#frame(frame), () => {
444
+ this.#waitingForKeyframe = true;
445
+ this.#requestKeyframe();
446
+ });
447
+ this.#jitter = new JitterBuffer({
448
+ latency: JITTER_LATENCY_MS,
449
+ output: (packet) => this.#assembler?.push(packet),
450
+ });
451
+ this.#requestKeyframe();
452
+ }
453
+ this.#jitter.push({
454
+ sequenceNumber: header.sequenceNumber,
455
+ timestamp: header.timestamp,
456
+ marker: header.marker,
457
+ payload: rtp.payload,
458
+ });
459
+ }
460
+ #frame(frame) {
461
+ const wc = this.#wc;
462
+ if (!wc || !this.#active)
463
+ return;
464
+ if (this.#waitingForKeyframe && !frame.keyframe) {
465
+ this.#skippedDeltas += 1;
466
+ return;
467
+ }
468
+ if (!this.#decoder || this.#decoder.state === "closed") {
469
+ if (!frame.keyframe) {
470
+ this.#skippedDeltas += 1;
471
+ return;
472
+ }
473
+ this.#decoder = this.#createDecoder(wc);
474
+ }
475
+ if (frame.keyframe) {
476
+ this.#waitingForKeyframe = false;
477
+ this.#stopKeyframeTimer();
478
+ }
479
+ const timestampUs = this.#timestampUs(frame.timestamp);
480
+ try {
481
+ this.#decoder.decode(new wc.EncodedVideoChunk({
482
+ type: frame.keyframe ? "key" : "delta",
483
+ timestamp: timestampUs,
484
+ data: frame.data,
485
+ }));
486
+ }
487
+ catch {
488
+ this.#decodeFailed();
489
+ }
490
+ }
491
+ #createDecoder(wc) {
492
+ const decoder = new wc.VideoDecoder({
493
+ output: (output) => {
494
+ const rotation = this.#rotation;
495
+ this.#outputChain = this.#outputChain.then(() => this.#emit(output, rotation));
496
+ },
497
+ error: () => {
498
+ if (this.#decoder === decoder)
499
+ this.#decodeFailed();
500
+ },
501
+ });
502
+ decoder.configure({ codec: this.#codecString, hardwareAcceleration: "prefer-software", optimizeForLatency: true });
503
+ return decoder;
504
+ }
505
+ async #emit(output, rotation) {
506
+ try {
507
+ const width = output.codedWidth;
508
+ const height = output.codedHeight;
509
+ const options = { format: "I420" };
510
+ const data = new Uint8Array(output.allocationSize(options));
511
+ await output.copyTo(data, options);
512
+ const tight = videoFrameLength(VideoBufferType.I420, width, height);
513
+ const frame = new VideoFrame(data.length === tight ? data : data.subarray(0, tight), width, height, VideoBufferType.I420);
514
+ this.#width = width;
515
+ this.#height = height;
516
+ this.#decoded.mark();
517
+ const event = { frame, timestampUs: BigInt(output.timestamp), rotation };
518
+ if (!this.#stopped && this.#active)
519
+ this.onframe?.(event);
520
+ }
521
+ catch {
522
+ this.#decodeErrors += 1;
523
+ }
524
+ finally {
525
+ output.close();
526
+ }
527
+ }
528
+ #decodeFailed() {
529
+ this.#decodeErrors += 1;
530
+ const decoder = this.#decoder;
531
+ this.#decoder = undefined;
532
+ if (decoder && decoder.state !== "closed") {
533
+ try {
534
+ decoder.close();
535
+ }
536
+ catch { /* already closed */ }
537
+ }
538
+ this.#waitingForKeyframe = true;
539
+ this.#requestKeyframe();
540
+ }
541
+ /** RTP timestamp (90 kHz, wrapping at 2^32) to microseconds since this stream's first frame. */
542
+ #timestampUs(rtpTimestamp) {
543
+ if (this.#firstRtpTimestamp === undefined)
544
+ this.#firstRtpTimestamp = rtpTimestamp;
545
+ if (rtpTimestamp < this.#lastRtpTimestamp && this.#lastRtpTimestamp - rtpTimestamp > 0x80000000) {
546
+ this.#rtpTimestampWraps += 1;
547
+ }
548
+ this.#lastRtpTimestamp = rtpTimestamp;
549
+ const ticks = this.#rtpTimestampWraps * 0x100000000 + rtpTimestamp - this.#firstRtpTimestamp;
550
+ return Math.round((ticks * 1_000_000) / VIDEO_CLOCK_RATE);
551
+ }
552
+ /**
553
+ * PLI to the sender, at once unless one left in the last 1 s; while no
554
+ * keyframe has arrived, a timer repeats it every 1 s.
555
+ */
556
+ #requestKeyframe() {
557
+ if (this.#stopped || !this.#active)
558
+ return;
559
+ if (Date.now() - this.#lastKeyframeRequestAt >= KEYFRAME_REQUEST_INTERVAL_MS)
560
+ this.#sendPli();
561
+ if (this.#keyframeTimer)
562
+ return;
563
+ this.#keyframeTimer = setInterval(() => {
564
+ if (!this.#waitingForKeyframe || !this.#active) {
565
+ this.#stopKeyframeTimer();
566
+ return;
567
+ }
568
+ this.#sendPli();
569
+ }, KEYFRAME_REQUEST_INTERVAL_MS);
570
+ this.#keyframeTimer.unref?.();
571
+ }
572
+ #sendPli() {
573
+ const ssrc = this.#ssrc ?? this.#track.ssrc;
574
+ const receiver = this.#transceiver?.receiver;
575
+ if (ssrc === undefined || !receiver)
576
+ return;
577
+ this.#lastKeyframeRequestAt = Date.now();
578
+ this.#keyframeRequests += 1;
579
+ void receiver.sendRtcpPLI(ssrc).catch(() => undefined);
580
+ }
581
+ #stopKeyframeTimer() {
582
+ if (!this.#keyframeTimer)
583
+ return;
584
+ clearInterval(this.#keyframeTimer);
585
+ this.#keyframeTimer = undefined;
586
+ }
587
+ #resetPipeline() {
588
+ this.#stopKeyframeTimer();
589
+ this.#jitter?.close();
590
+ this.#jitter = undefined;
591
+ this.#assembler?.reset();
592
+ const decoder = this.#decoder;
593
+ this.#decoder = undefined;
594
+ if (decoder && decoder.state !== "closed") {
595
+ try {
596
+ decoder.close();
597
+ }
598
+ catch { /* already closed */ }
599
+ }
600
+ this.#waitingForKeyframe = true;
601
+ }
602
+ }
603
+ export const createWeriftVideoFactory = () => ({
604
+ createVideoSender: async (options) => new WeriftVideoSender(await loadWebCodecs(), options),
605
+ createVideoReceiver: (track, transceiver) => new WeriftVideoReceiver(track, transceiver),
606
+ });
607
+ //# sourceMappingURL=engine-werift-video.js.map