@zhivex-ai/core 1.3.0 → 1.4.0

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.
package/dist/realtime.js CHANGED
@@ -1,5 +1,19 @@
1
1
  import { BoundedReplayBroadcast, StreamBufferOverflowError } from "./bounded-broadcast.js";
2
- import { ConfigurationError, UnsupportedFeatureError, ValidationError } from "./errors.js";
2
+ import { ConfigurationError, ConflictError, UnsupportedFeatureError, ValidationError } from "./errors.js";
3
+ const asError = (error) => error instanceof Error ? error : new Error(String(error));
4
+ const canonicalRealtimeValue = (value) => {
5
+ if (value === undefined)
6
+ return "undefined";
7
+ if (value === null || typeof value !== "object")
8
+ return JSON.stringify(value) ?? String(value);
9
+ if (Array.isArray(value))
10
+ return `[${value.map(canonicalRealtimeValue).join(",")}]`;
11
+ return `{${Object.keys(value)
12
+ .sort()
13
+ .map((key) => `${JSON.stringify(key)}:${canonicalRealtimeValue(value[key])}`)
14
+ .join(",")}}`;
15
+ };
16
+ const realtimeToolCallFingerprint = (event) => `${JSON.stringify(event.toolCall.name)}:${canonicalRealtimeValue(event.toolCall.input)}`;
3
17
  export class CallbackRealtimeSession {
4
18
  provider;
5
19
  modelId;
@@ -7,9 +21,17 @@ export class CallbackRealtimeSession {
7
21
  config;
8
22
  connection;
9
23
  callbacks;
24
+ initializationTimeoutMs;
10
25
  broadcast = new BoundedReplayBroadcast();
11
- receiverPromise;
12
- closed = false;
26
+ seenToolCalls = new Map();
27
+ state = "new";
28
+ initializationPromise;
29
+ terminationPromise;
30
+ terminationError;
31
+ readyPromise;
32
+ resolveReady;
33
+ rejectReady;
34
+ ready = false;
13
35
  ended = false;
14
36
  constructor(options) {
15
37
  this.provider = options.provider;
@@ -18,80 +40,158 @@ export class CallbackRealtimeSession {
18
40
  this.config = options.config;
19
41
  this.connection = options.connection;
20
42
  this.callbacks = options.callbacks;
43
+ if (options.initializationTimeoutMs !== undefined &&
44
+ (!Number.isSafeInteger(options.initializationTimeoutMs) || options.initializationTimeoutMs <= 0)) {
45
+ throw new ConfigurationError("Realtime initialization timeout must be a positive safe integer.");
46
+ }
47
+ this.initializationTimeoutMs = options.initializationTimeoutMs;
48
+ if (this.callbacks.isReadyPayload) {
49
+ this.readyPromise = new Promise((resolve, reject) => {
50
+ this.resolveReady = resolve;
51
+ this.rejectReady = reject;
52
+ });
53
+ }
21
54
  }
22
- async initialize() {
23
- if (this.callbacks.buildInitialPayloads) {
24
- await this.sendPayloads(this.callbacks.buildInitialPayloads(this.config, this.config));
55
+ initialize() {
56
+ if (this.state === "open") {
57
+ return Promise.resolve();
25
58
  }
26
- const event = {
27
- type: "realtime-start"
28
- };
29
- await this.broadcast.publish(event);
30
- if (!this.receiverPromise) {
31
- this.receiverPromise = this.receiveLoop();
59
+ if (this.initializationPromise) {
60
+ return this.initializationPromise;
32
61
  }
62
+ if (this.state !== "new") {
63
+ return Promise.reject(new ConfigurationError("Realtime session is already closing or closed."));
64
+ }
65
+ this.state = "initializing";
66
+ this.initializationPromise = this.start();
67
+ return this.initializationPromise;
33
68
  }
34
69
  async sendAudio(frame) {
35
- await this.sendPayloads(this.callbacks.buildAudioPayloads(frame, this.config));
70
+ this.assertOpen();
71
+ await this.sendBuiltPayloads(() => this.callbacks.buildAudioPayloads(frame, this.config));
36
72
  }
37
73
  async sendMedia(frame) {
74
+ this.assertOpen();
38
75
  if (frame.mediaType.startsWith("audio/")) {
39
- await this.sendAudio(frame);
76
+ await this.sendBuiltPayloads(() => this.callbacks.buildAudioPayloads(frame, this.config));
40
77
  return;
41
78
  }
42
79
  if (!this.callbacks.buildMediaPayloads) {
43
80
  throw new UnsupportedFeatureError(`Realtime media input is not supported for provider "${this.provider}" with media type "${frame.mediaType}".`);
44
81
  }
45
- await this.sendPayloads(this.callbacks.buildMediaPayloads(frame, this.config));
82
+ await this.sendBuiltPayloads(() => this.callbacks.buildMediaPayloads(frame, this.config));
46
83
  }
47
84
  async sendText(text) {
48
- await this.sendPayloads(this.callbacks.buildTextPayloads(text, this.config));
85
+ this.assertOpen();
86
+ await this.sendBuiltPayloads(() => this.callbacks.buildTextPayloads(text, this.config));
49
87
  }
50
88
  async sendToolResult(result) {
51
- await this.sendPayloads(this.callbacks.buildToolResultPayloads(result, this.config));
52
- if (!this.broadcast.isClosed) {
89
+ this.assertOpen();
90
+ const payloads = this.callbacks.buildToolResultPayloads(result, this.config);
91
+ try {
92
+ await this.sendPayloads(payloads);
53
93
  await this.broadcast.publish({
54
94
  type: "realtime-tool-result",
55
95
  toolResult: result
56
96
  });
57
97
  }
98
+ catch (error) {
99
+ await this.terminate({ reason: "error", error });
100
+ throw error;
101
+ }
58
102
  }
59
103
  async update(config) {
60
- this.config = {
104
+ this.assertOpen();
105
+ const nextConfig = {
61
106
  ...this.config,
62
107
  ...config
63
108
  };
64
- await this.sendPayloads(this.callbacks.buildUpdatePayloads(this.config, this.config));
109
+ await this.sendBuiltPayloads(() => this.callbacks.buildUpdatePayloads(nextConfig, nextConfig));
110
+ this.config = nextConfig;
65
111
  }
66
112
  eventStream() {
67
113
  return this.broadcast.stream();
68
114
  }
69
115
  async close() {
70
- if (this.closed) {
116
+ if (this.state === "closed") {
71
117
  return;
72
118
  }
73
- this.closed = true;
119
+ const initiatedTermination = !this.terminationPromise;
120
+ const sendClosePayloads = this.state === "open";
121
+ await this.terminate({
122
+ reason: "client-close",
123
+ sendClosePayloads
124
+ });
125
+ if (initiatedTermination && this.terminationError) {
126
+ throw this.terminationError;
127
+ }
128
+ }
129
+ async start() {
74
130
  try {
75
- if (this.callbacks.buildClosePayloads) {
76
- await this.sendPayloads(this.callbacks.buildClosePayloads(this.config, this.config));
131
+ if (this.callbacks.buildInitialPayloads) {
132
+ await this.sendPayloads(this.callbacks.buildInitialPayloads(this.config, this.config));
77
133
  }
78
- }
79
- finally {
80
- await this.connection.close();
81
- try {
82
- await this.receiverPromise;
134
+ if (this.state !== "initializing") {
135
+ throw new ConfigurationError("Realtime session was closed during initialization.");
83
136
  }
84
- catch {
85
- // ignore connection shutdown errors
137
+ if (this.readyPromise) {
138
+ this.state = "handshaking";
139
+ void this.receiveLoop();
140
+ if (this.initializationTimeoutMs === undefined) {
141
+ await this.readyPromise;
142
+ }
143
+ else {
144
+ let timer;
145
+ try {
146
+ await Promise.race([
147
+ this.readyPromise,
148
+ new Promise((_, reject) => {
149
+ timer = setTimeout(() => reject(new Error(`Realtime provider setup timed out after ${this.initializationTimeoutMs}ms.`)), this.initializationTimeoutMs);
150
+ })
151
+ ]);
152
+ }
153
+ finally {
154
+ if (timer)
155
+ clearTimeout(timer);
156
+ }
157
+ }
158
+ if (this.state !== "handshaking") {
159
+ throw this.terminationError ?? new ConfigurationError("Realtime session was closed during initialization.");
160
+ }
161
+ this.state = "open";
86
162
  }
87
- if (!this.ended) {
88
- this.ended = true;
89
- await this.broadcast.publish({
90
- type: "realtime-end",
91
- reason: "client-close"
92
- }, { terminal: true });
163
+ const event = {
164
+ type: "realtime-start"
165
+ };
166
+ await this.broadcast.publish(event);
167
+ if (!this.readyPromise) {
168
+ if (this.state !== "initializing") {
169
+ throw new ConfigurationError("Realtime session was closed during initialization.");
170
+ }
171
+ this.state = "open";
172
+ void this.receiveLoop();
173
+ }
174
+ }
175
+ catch (error) {
176
+ if (this.state !== "closing" && this.state !== "closed") {
177
+ await this.terminate({ reason: "error", error });
93
178
  }
94
- await this.broadcast.close();
179
+ throw error;
180
+ }
181
+ }
182
+ assertOpen() {
183
+ if (this.state !== "open") {
184
+ throw new ConfigurationError("Realtime session is not open.");
185
+ }
186
+ }
187
+ async sendBuiltPayloads(build) {
188
+ const payloads = build();
189
+ try {
190
+ await this.sendPayloads(payloads);
191
+ }
192
+ catch (error) {
193
+ await this.terminate({ reason: "error", error });
194
+ throw error;
95
195
  }
96
196
  }
97
197
  async sendPayloads(payloads) {
@@ -101,131 +201,285 @@ export class CallbackRealtimeSession {
101
201
  }
102
202
  async receiveLoop() {
103
203
  try {
104
- while (true) {
204
+ while (this.state === "open" || this.state === "handshaking") {
105
205
  const payload = await this.connection.recvJson();
206
+ if (this.state !== "open" && this.state !== "handshaking") {
207
+ return;
208
+ }
106
209
  if (payload == null) {
107
- break;
210
+ await this.terminate({ reason: "connection-closed" });
211
+ return;
108
212
  }
109
- for (const event of this.callbacks.parseEvent((payload ?? {}))) {
110
- if (event.type === "realtime-end") {
111
- this.ended = true;
213
+ const record = (payload ?? {});
214
+ if (!this.ready && this.callbacks.isReadyPayload?.(record)) {
215
+ this.ready = true;
216
+ this.resolveReady?.();
217
+ }
218
+ for (const event of this.callbacks.parseEvent(record)) {
219
+ if (event.type === "realtime-tool-call") {
220
+ const fingerprint = realtimeToolCallFingerprint(event);
221
+ const previous = this.seenToolCalls.get(event.toolCall.id);
222
+ if (previous !== undefined) {
223
+ if (previous !== fingerprint) {
224
+ await this.terminate({
225
+ reason: "error",
226
+ error: new ConflictError(`Realtime tool call id "${event.toolCall.id}" was reused with a different payload.`)
227
+ });
228
+ return;
229
+ }
230
+ continue;
231
+ }
232
+ this.seenToolCalls.set(event.toolCall.id, fingerprint);
233
+ }
234
+ if (event.type === "realtime-error") {
235
+ await this.terminate({
236
+ reason: "error",
237
+ errorEvent: event
238
+ });
239
+ return;
112
240
  }
113
- await this.broadcast.publish(event, { terminal: event.type === "realtime-end" });
114
241
  if (event.type === "realtime-end") {
115
- await this.broadcast.close();
242
+ await this.terminate({
243
+ reason: event.reason ?? "connection-closed",
244
+ endEvent: event
245
+ });
246
+ return;
247
+ }
248
+ if (this.state !== "open" && this.state !== "handshaking") {
116
249
  return;
117
250
  }
251
+ await this.broadcast.publish(event);
118
252
  }
119
253
  }
120
- if (!this.ended) {
121
- this.ended = true;
122
- await this.broadcast.publish({
123
- type: "realtime-end",
124
- reason: "connection-closed"
125
- }, { terminal: true });
126
- }
127
254
  }
128
255
  catch (error) {
129
- if (error instanceof StreamBufferOverflowError) {
130
- this.closed = true;
131
- this.ended = true;
132
- this.broadcast.fail(error);
133
- await this.connection.close();
256
+ if (this.state === "closing" || this.state === "closed") {
134
257
  return;
135
258
  }
136
- const event = {
259
+ await this.terminate({ reason: "error", error });
260
+ }
261
+ }
262
+ terminate(options) {
263
+ if (this.terminationPromise) {
264
+ return this.terminationPromise;
265
+ }
266
+ this.state = "closing";
267
+ this.terminationPromise = this.finishTermination(options);
268
+ return this.terminationPromise;
269
+ }
270
+ async finishTermination(options) {
271
+ let errorEvent = options.errorEvent;
272
+ let failure = options.error === undefined ? undefined : asError(options.error);
273
+ if (options.sendClosePayloads && this.callbacks.buildClosePayloads) {
274
+ try {
275
+ await this.sendPayloads(this.callbacks.buildClosePayloads(this.config, this.config));
276
+ }
277
+ catch (error) {
278
+ failure = asError(error);
279
+ }
280
+ }
281
+ try {
282
+ await this.connection.close();
283
+ }
284
+ catch (error) {
285
+ failure ??= asError(error);
286
+ }
287
+ if (!errorEvent && failure) {
288
+ errorEvent = {
137
289
  type: "realtime-error",
138
- error: error instanceof Error ? error : new Error(String(error)),
139
- message: error instanceof Error ? error.message : String(error)
290
+ error: failure,
291
+ message: failure.message
140
292
  };
141
- await this.broadcast.publish(event, { terminal: true });
142
- if (!this.ended) {
143
- this.ended = true;
144
- const ended = {
145
- type: "realtime-end",
146
- reason: "error",
147
- providerMetadata: {
148
- message: event.message ?? ""
293
+ }
294
+ if (!this.ready && this.readyPromise) {
295
+ const readinessError = failure ?? errorEvent?.error ?? new ConfigurationError(`Realtime session ended before provider "${this.provider}" acknowledged setup.`);
296
+ this.rejectReady?.(asError(readinessError));
297
+ }
298
+ const terminalEvents = [];
299
+ if (errorEvent) {
300
+ terminalEvents.push(errorEvent);
301
+ }
302
+ if (!this.ended) {
303
+ this.ended = true;
304
+ terminalEvents.push(options.endEvent ?? {
305
+ type: "realtime-end",
306
+ reason: errorEvent ? "error" : options.reason,
307
+ ...(errorEvent
308
+ ? {
309
+ providerMetadata: {
310
+ message: errorEvent.message ?? errorEvent.error?.message ?? ""
311
+ }
149
312
  }
150
- };
151
- await this.broadcast.publish(ended, { terminal: true });
313
+ : {})
314
+ });
315
+ }
316
+ try {
317
+ for (const event of terminalEvents) {
318
+ await this.broadcast.publish(event, { terminal: true });
319
+ }
320
+ }
321
+ catch (error) {
322
+ const publishFailure = asError(error);
323
+ failure ??= publishFailure;
324
+ if (!this.broadcast.isClosed) {
325
+ this.broadcast.fail(publishFailure);
152
326
  }
153
327
  }
154
328
  finally {
155
- await this.broadcast.close();
329
+ this.broadcast.close();
330
+ this.state = "closed";
156
331
  }
332
+ this.terminationError = failure;
157
333
  }
158
334
  }
159
335
  class BrowserRealtimeConnection {
160
336
  socket;
161
337
  queue = [];
162
- resolvers = [];
338
+ waiters = [];
163
339
  closed = false;
164
340
  queueFailure;
165
341
  maxIncomingFrameBytes;
166
- constructor(socket, maxIncomingFrameBytes) {
342
+ signal;
343
+ onAbort;
344
+ constructor(socket, maxIncomingFrameBytes, signal) {
167
345
  this.socket = socket;
168
346
  this.maxIncomingFrameBytes = maxIncomingFrameBytes;
347
+ this.signal = signal;
348
+ this.onAbort = () => {
349
+ this.fail(signal?.reason instanceof Error
350
+ ? signal.reason
351
+ : new DOMException("The realtime connection was aborted.", "AbortError"));
352
+ };
169
353
  socket.onmessage = (event) => {
354
+ if (this.closed) {
355
+ return;
356
+ }
170
357
  const value = event.data;
171
358
  if (incomingFrameBytes(value) > this.maxIncomingFrameBytes) {
172
- this.queueFailure = new ValidationError(`Realtime frame exceeds the ${this.maxIncomingFrameBytes}-byte limit.`);
173
- this.closed = true;
174
- while (this.resolvers.length > 0) {
175
- this.resolvers.shift()(undefined);
176
- }
177
- this.socket.close();
359
+ this.fail(new ValidationError(`Realtime frame exceeds the ${this.maxIncomingFrameBytes}-byte limit.`));
178
360
  return;
179
361
  }
180
- if (this.resolvers.length > 0) {
181
- this.resolvers.shift()(value);
362
+ if (this.waiters.length > 0) {
363
+ this.waiters.shift().resolve(value);
182
364
  }
183
365
  else {
184
366
  if (this.queue.length >= 256) {
185
- this.queueFailure = new StreamBufferOverflowError(256);
186
- this.closed = true;
187
- this.socket.close();
367
+ this.fail(new StreamBufferOverflowError(256));
188
368
  return;
189
369
  }
190
370
  this.queue.push(value);
191
371
  }
192
372
  };
193
- socket.onclose = () => {
194
- this.closed = true;
195
- while (this.resolvers.length > 0) {
196
- this.resolvers.shift()(undefined);
373
+ socket.onclose = (event) => {
374
+ if (!this.closed &&
375
+ ((typeof event.code === "number" && event.code !== 1_000) || event.wasClean === false)) {
376
+ const details = [
377
+ typeof event.code === "number" ? `code ${event.code}` : undefined,
378
+ event.reason?.trim() || undefined
379
+ ].filter(Boolean).join(": ");
380
+ this.fail(new Error(`Realtime WebSocket closed unexpectedly${details ? ` (${details})` : ""}.`));
381
+ return;
197
382
  }
383
+ this.finish();
198
384
  };
199
385
  socket.onerror = () => {
200
- this.closed = true;
201
- while (this.resolvers.length > 0) {
202
- this.resolvers.shift()(undefined);
203
- }
386
+ this.fail(new Error("Realtime WebSocket connection failed."));
204
387
  };
388
+ if (signal?.aborted) {
389
+ this.onAbort();
390
+ }
391
+ else {
392
+ signal?.addEventListener("abort", this.onAbort, { once: true });
393
+ }
205
394
  }
206
395
  async sendJson(payload) {
207
- this.socket.send(JSON.stringify(payload));
396
+ if (this.queueFailure) {
397
+ throw this.queueFailure;
398
+ }
399
+ if (this.closed) {
400
+ throw new Error("Realtime connection is closed.");
401
+ }
402
+ try {
403
+ this.socket.send(JSON.stringify(payload));
404
+ }
405
+ catch (error) {
406
+ const failure = asError(error);
407
+ this.fail(failure);
408
+ throw failure;
409
+ }
208
410
  }
209
411
  async recvJson() {
210
412
  if (this.queueFailure) {
211
413
  throw this.queueFailure;
212
414
  }
213
415
  if (this.queue.length > 0) {
214
- return parseIncoming(this.queue.shift(), this.maxIncomingFrameBytes);
416
+ return this.parseFrame(this.queue.shift());
215
417
  }
216
418
  if (this.closed) {
217
419
  return undefined;
218
420
  }
219
- const next = await new Promise((resolve) => {
220
- this.resolvers.push(resolve);
421
+ const next = await new Promise((resolve, reject) => {
422
+ this.waiters.push({ resolve, reject });
221
423
  });
222
424
  if (this.queueFailure) {
223
425
  throw this.queueFailure;
224
426
  }
225
- return parseIncoming(next, this.maxIncomingFrameBytes);
427
+ return this.parseFrame(next);
226
428
  }
227
429
  async close() {
228
- this.socket.close();
430
+ if (this.closed) {
431
+ return;
432
+ }
433
+ this.finish();
434
+ try {
435
+ this.socket.close();
436
+ }
437
+ catch (error) {
438
+ const failure = asError(error);
439
+ this.queueFailure = failure;
440
+ throw failure;
441
+ }
442
+ }
443
+ async parseFrame(value) {
444
+ try {
445
+ return await parseIncoming(value, this.maxIncomingFrameBytes);
446
+ }
447
+ catch (error) {
448
+ const failure = asError(error);
449
+ this.fail(failure);
450
+ throw failure;
451
+ }
452
+ }
453
+ finish() {
454
+ if (this.closed) {
455
+ return;
456
+ }
457
+ this.closed = true;
458
+ this.cleanupSignal();
459
+ while (this.waiters.length > 0) {
460
+ this.waiters.shift().resolve(undefined);
461
+ }
462
+ }
463
+ fail(error) {
464
+ if (this.queueFailure || this.closed) {
465
+ return;
466
+ }
467
+ this.queueFailure = error;
468
+ this.closed = true;
469
+ this.queue.length = 0;
470
+ this.cleanupSignal();
471
+ while (this.waiters.length > 0) {
472
+ this.waiters.shift().reject(error);
473
+ }
474
+ try {
475
+ this.socket.close();
476
+ }
477
+ catch {
478
+ // The original transport error remains the actionable failure.
479
+ }
480
+ }
481
+ cleanupSignal() {
482
+ this.signal?.removeEventListener("abort", this.onAbort);
229
483
  }
230
484
  }
231
485
  const incomingFrameBytes = (value) => {
@@ -298,6 +552,9 @@ const waitForOpen = (socket, signal, timeoutMs) => new Promise((resolve, reject)
298
552
  socket.onerror = () => {
299
553
  fail("Realtime connection failed.");
300
554
  };
555
+ socket.onclose = () => {
556
+ fail("Realtime connection closed before opening.");
557
+ };
301
558
  if (signal?.aborted) {
302
559
  onAbort();
303
560
  }
@@ -317,9 +574,13 @@ export const openWebSocketConnection = async (url, headers, options) => {
317
574
  if (!Number.isSafeInteger(maxIncomingFrameBytes) || maxIncomingFrameBytes <= 0) {
318
575
  throw new ConfigurationError('The realtime "maxIncomingFrameBytes" option must be a positive safe integer.');
319
576
  }
577
+ if (options?.timeoutMs !== undefined &&
578
+ (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0)) {
579
+ throw new ConfigurationError('The realtime "timeoutMs" option must be a positive safe integer.');
580
+ }
320
581
  const socket = new WebSocketCtor(url, options?.subprotocols);
321
582
  await waitForOpen(socket, options?.signal, options?.timeoutMs);
322
- return new BrowserRealtimeConnection(socket, maxIncomingFrameBytes);
583
+ return new BrowserRealtimeConnection(socket, maxIncomingFrameBytes, options?.signal);
323
584
  };
324
585
  export const unsupportedBrowserToken = async () => {
325
586
  throw new UnsupportedFeatureError("This realtime model does not support browser session tokens.");
@@ -329,7 +590,25 @@ const encodeRealtimeFrameData = (data) => {
329
590
  return data;
330
591
  }
331
592
  const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
332
- return Buffer.from(bytes).toString("base64");
593
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
594
+ const chunks = [];
595
+ const inputChunkBytes = 12 * 1024;
596
+ for (let chunkStart = 0; chunkStart < bytes.length; chunkStart += inputChunkBytes) {
597
+ const chunkEnd = Math.min(chunkStart + inputChunkBytes, bytes.length);
598
+ let encodedChunk = "";
599
+ for (let index = chunkStart; index < chunkEnd; index += 3) {
600
+ const first = bytes[index] ?? 0;
601
+ const second = bytes[index + 1];
602
+ const third = bytes[index + 2];
603
+ const value = (first << 16) | ((second ?? 0) << 8) | (third ?? 0);
604
+ encodedChunk += alphabet[(value >>> 18) & 0x3f];
605
+ encodedChunk += alphabet[(value >>> 12) & 0x3f];
606
+ encodedChunk += second === undefined ? "=" : alphabet[(value >>> 6) & 0x3f];
607
+ encodedChunk += third === undefined ? "=" : alphabet[value & 0x3f];
608
+ }
609
+ chunks.push(encodedChunk);
610
+ }
611
+ return chunks.join("");
333
612
  };
334
613
  export const encodeAudioFrame = (frame) => encodeRealtimeFrameData(frame.data);
335
614
  export const encodeMediaFrame = (frame) => encodeRealtimeFrameData(frame.data);