realtime-avatar 0.5.0 → 0.5.2
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/README.md +18 -4
- package/dist/express.d.ts +2 -2
- package/dist/express.js +142 -7
- package/dist/hono.d.ts +2 -2
- package/dist/hono.js +142 -7
- package/dist/index.d.ts +127 -11
- package/dist/index.js +142 -7
- package/dist/nextjs.d.ts +2 -2
- package/dist/nextjs.js +142 -7
- package/dist/{proxy-client-Dm-O0jNn.d.ts → proxy-client-BIaUo0az.d.ts} +147 -1
- package/dist/react-native.d.ts +10 -2
- package/dist/react-native.js +121 -1
- package/dist/react.d.ts +2 -2
- package/dist/react.js +132 -2
- package/dist/server.d.ts +1 -1
- package/dist/server.js +142 -7
- package/dist/tanstack-start.d.ts +2 -2
- package/dist/tanstack-start.js +142 -7
- package/dist/{types-DLxz6uFE.d.ts → types-BeGuvyra.d.ts} +19 -2
- package/dist/{types-B45GgjrV.d.ts → types-CHrhuhYb.d.ts} +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -43,7 +43,7 @@ var RealtimeAvatarHttpError = class extends RealtimeAvatarError {
|
|
|
43
43
|
|
|
44
44
|
// ../http-client/src/client.ts
|
|
45
45
|
var DEFAULT_BASE_URL = "https://realtimeavatar.ai/api/v1";
|
|
46
|
-
var SDK_VERSION = "0.5.
|
|
46
|
+
var SDK_VERSION = "0.5.2";
|
|
47
47
|
var RealtimeAvatar = class {
|
|
48
48
|
#apiKey;
|
|
49
49
|
#baseUrl;
|
|
@@ -157,10 +157,15 @@ var RealtimeAvatar = class {
|
|
|
157
157
|
/**
|
|
158
158
|
* Register a character from a looping clip you host.
|
|
159
159
|
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
160
|
+
* @deprecated CLOSED to new callers — this answers `422` unless your tenant was already
|
|
161
|
+
* creating from video, in which case it keeps working and your existing avatars are
|
|
162
|
+
* untouched. Use {@link createAvatarFromImage}: one still, and the platform renders the
|
|
163
|
+
* resting loop and the motion library from it.
|
|
164
|
+
*
|
|
165
|
+
* The reason it closed is not arbitrary. Every clip has to start and end on ONE rest pose
|
|
166
|
+
* or a state switch reads as a jump, and the platform can only guarantee that when it
|
|
167
|
+
* rendered the loop and the clips from the same portrait. A supplied video cannot honour
|
|
168
|
+
* it, so the lane could never be made to look right.
|
|
164
169
|
*/
|
|
165
170
|
async createAvatarFromVideo(input) {
|
|
166
171
|
const asset = await this.createRemoteAsset({ kind: "video", remoteUrl: input.videoUrl });
|
|
@@ -173,12 +178,41 @@ var RealtimeAvatar = class {
|
|
|
173
178
|
metadata: input.metadata
|
|
174
179
|
});
|
|
175
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* Register a character from ONE still image. The platform generates everything moving:
|
|
183
|
+
* the resting loop she idles in, then a starter motion library rendered against her rest
|
|
184
|
+
* pose. No footage, no clip URLs, nothing to shoot.
|
|
185
|
+
*
|
|
186
|
+
* `motionPrompt` directs the RESTING LOOP — the video she plays when nothing else is
|
|
187
|
+
* happening — and it is the only chance to direct it, because there is no API today that
|
|
188
|
+
* re-generates a loop after creation (see {@link updateAvatar} for the one thing that can
|
|
189
|
+
* be re-pointed). Describe a small closed arc that returns to where it started: "settles
|
|
190
|
+
* into frame, breathes gently, a slow blink". Omit it and the house default is used.
|
|
191
|
+
*
|
|
192
|
+
* Creation returns while the avatar is still `preprocessing`; poll {@link getAvatar} until
|
|
193
|
+
* it leaves that state. The loop is load-bearing, so a failure there settles `failed` with
|
|
194
|
+
* a readable error — the motion library is not, and a library failure degrades to
|
|
195
|
+
* loop-only rather than demoting the character.
|
|
196
|
+
*/
|
|
197
|
+
async createAvatarFromImage(input) {
|
|
198
|
+
const asset = await this.createRemoteAsset({ kind: "image", remoteUrl: input.imageUrl });
|
|
199
|
+
return this.createAvatar({
|
|
200
|
+
displayName: input.displayName,
|
|
201
|
+
sourceKind: "image",
|
|
202
|
+
sourceAssetId: asset.id,
|
|
203
|
+
motionPrompt: input.motionPrompt,
|
|
204
|
+
voice: input.voice,
|
|
205
|
+
settings: input.settings,
|
|
206
|
+
metadata: input.metadata
|
|
207
|
+
});
|
|
208
|
+
}
|
|
176
209
|
async createAvatar(input) {
|
|
177
210
|
const body = {
|
|
178
211
|
displayName: input.displayName,
|
|
179
212
|
sourceKind: input.sourceKind,
|
|
180
213
|
sourceAssetId: input.sourceAssetId
|
|
181
214
|
};
|
|
215
|
+
if (input.motionPrompt !== void 0) body.motionPrompt = input.motionPrompt;
|
|
182
216
|
if (input.voice !== void 0) body.voice = input.voice;
|
|
183
217
|
if (input.settings !== void 0) body.settings = input.settings;
|
|
184
218
|
if (input.metadata !== void 0) body.metadata = input.metadata;
|
|
@@ -191,7 +225,14 @@ var RealtimeAvatar = class {
|
|
|
191
225
|
async getAvatar(avatarId) {
|
|
192
226
|
return toAvatar(await this.#json(await this.#request("GET", `/avatars/${avatarId}`)));
|
|
193
227
|
}
|
|
194
|
-
/**
|
|
228
|
+
/**
|
|
229
|
+
* Re-point what an avatar already is. `defaultVoiceId: null` clears the default voice.
|
|
230
|
+
*
|
|
231
|
+
* Cheap metadata only. The three things that cost a render have their own methods, because
|
|
232
|
+
* putting them in a patch alongside a rename would hide minutes of GPU work behind a field:
|
|
233
|
+
* {@link retimeAnchor} moves the rest frame, {@link swapSource} replaces the footage, and
|
|
234
|
+
* the resting loop is re-directed by description at `PUT /v1/avatars/{id}/loop`.
|
|
235
|
+
*/
|
|
195
236
|
async updateAvatar(avatarId, patch) {
|
|
196
237
|
return toAvatar(
|
|
197
238
|
await this.#json(await this.#request("PATCH", `/avatars/${avatarId}`, { json: patch }))
|
|
@@ -274,6 +315,94 @@ var RealtimeAvatar = class {
|
|
|
274
315
|
)
|
|
275
316
|
);
|
|
276
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* Re-direct the RESTING LOOP — the video she plays when nothing else is happening — from
|
|
320
|
+
* a new one-sentence description.
|
|
321
|
+
*
|
|
322
|
+
* Not a clip, and this is the distinction integrations get wrong: a clip with
|
|
323
|
+
* `role: "idle"` is a variant spliced OVER the loop, and declaring one never changes what
|
|
324
|
+
* she rests in. This is the only thing that does.
|
|
325
|
+
*
|
|
326
|
+
* `202`, because the render takes minutes. Three properties, all measured against a real
|
|
327
|
+
* render rather than asserted:
|
|
328
|
+
*
|
|
329
|
+
* - **She never goes dark.** She stays `ready` and keeps serving her previous loop for the
|
|
330
|
+
* entire render — returned as `servingUrl` — then the swap publishes in one step.
|
|
331
|
+
* - **Your clip library is untouched.** Clips render against the portrait, not against the
|
|
332
|
+
* loop, so a re-direct re-queues nothing and does not move `revision`.
|
|
333
|
+
* - **It bills once**, at the rendering model's rate, per re-direct.
|
|
334
|
+
*
|
|
335
|
+
* Refusals worth telling apart: `409 loop_pending` (one is already in flight — wait) and
|
|
336
|
+
* `422 loop_not_generatable` (a grandfathered video-sourced avatar has no portrait to
|
|
337
|
+
* re-animate — terminal, do not retry).
|
|
338
|
+
*/
|
|
339
|
+
async setLoop(avatarId, loop) {
|
|
340
|
+
return await this.#json(
|
|
341
|
+
await this.#request("PUT", `/avatars/${avatarId}/loop`, { json: { motionPrompt: loop.motionPrompt } })
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Block until a loop re-direct settles, and throw if it did not take.
|
|
346
|
+
*
|
|
347
|
+
* `setLoop` returns on ACCEPTANCE; the render runs for minutes afterwards. Every caller
|
|
348
|
+
* therefore writes the same polling loop, and the obvious version of it never terminates
|
|
349
|
+
* on failure — a failed re-direct leaves her `ready` (she is still serving the old loop)
|
|
350
|
+
* and writes nothing to `error`. `idleVideoStatus` is the only field that moves, which is
|
|
351
|
+
* why this exists rather than a doc line telling you to poll.
|
|
352
|
+
*
|
|
353
|
+
* Race-free without a baseline: the platform commits `queued` before `setLoop` returns, so
|
|
354
|
+
* by the time you can call this the status has already left `ready`.
|
|
355
|
+
*
|
|
356
|
+
* Resolves with the settled avatar — `sourceAssetId` is now the new loop. Throws on a
|
|
357
|
+
* failed render and on timeout; a timeout is not a failure, so re-poll or call again.
|
|
358
|
+
*/
|
|
359
|
+
async waitForLoop(avatarId, options = {}) {
|
|
360
|
+
const timeoutMs = options.timeoutMs ?? 20 * 6e4;
|
|
361
|
+
const pollMs = Math.max(1e3, options.pollMs ?? 1e4);
|
|
362
|
+
const deadline = Date.now() + timeoutMs;
|
|
363
|
+
for (; ; ) {
|
|
364
|
+
const avatar = await this.getAvatar(avatarId);
|
|
365
|
+
if (avatar.idleVideoStatus === "failed") {
|
|
366
|
+
throw new RealtimeAvatarError(
|
|
367
|
+
`Loop re-direct failed for ${avatarId}. She is still serving her previous loop \u2014 nothing was lost, and you can send another description.`
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
if (avatar.idleVideoStatus === "ready" || avatar.idleVideoStatus === "none") return avatar;
|
|
371
|
+
if (Date.now() >= deadline) {
|
|
372
|
+
throw new RealtimeAvatarError(
|
|
373
|
+
`Loop re-direct for ${avatarId} was still ${avatar.idleVideoStatus} after ${Math.round(timeoutMs / 1e3)}s. This is a timeout, not a failure \u2014 the render may still land.`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
await sleep(pollMs);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Block until every clip in the library has stopped moving.
|
|
381
|
+
*
|
|
382
|
+
* Settled means no row is `queued` or `generating` — NOT that every row is `ready`.
|
|
383
|
+
* Waiting for all-`ready` is the intuitive version and it hangs forever: a clip rejected
|
|
384
|
+
* by pose validation settles `failed`, which is terminal. So this returns the library
|
|
385
|
+
* with the failures in it and lets you decide; a partial library is a legitimate outcome
|
|
386
|
+
* and the rest of it is already serving.
|
|
387
|
+
*
|
|
388
|
+
* Throws only on timeout.
|
|
389
|
+
*/
|
|
390
|
+
async waitForClips(avatarId, options = {}) {
|
|
391
|
+
const timeoutMs = options.timeoutMs ?? 20 * 6e4;
|
|
392
|
+
const pollMs = Math.max(1e3, options.pollMs ?? 1e4);
|
|
393
|
+
const deadline = Date.now() + timeoutMs;
|
|
394
|
+
for (; ; ) {
|
|
395
|
+
const library = await this.listClips(avatarId);
|
|
396
|
+
const moving = library.data.filter((c) => c.status === "queued" || c.status === "generating");
|
|
397
|
+
if (moving.length === 0) return library;
|
|
398
|
+
if (Date.now() >= deadline) {
|
|
399
|
+
throw new RealtimeAvatarError(
|
|
400
|
+
`${moving.length} clip(s) on ${avatarId} were still rendering after ${Math.round(timeoutMs / 1e3)}s: ${moving.map((c) => c.clipId).join(", ")}. This is a timeout, not a failure.`
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
await sleep(pollMs);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
277
406
|
/** The avatar's clip library: every non-retired clip, plus revision, anchor and eligibility. */
|
|
278
407
|
async listClips(avatarId) {
|
|
279
408
|
return this.#clipEnvelope(
|
|
@@ -475,7 +604,13 @@ function toAvatar(raw) {
|
|
|
475
604
|
sourceAssetId: a.sourceAssetId ? String(a.sourceAssetId) : null,
|
|
476
605
|
// Carried because it is the ONLY channel a failed source swap has: she stays `ready`
|
|
477
606
|
// and serving, and this says why the re-shoot did not take.
|
|
478
|
-
error: a.error ? String(a.error) : null
|
|
607
|
+
error: a.error ? String(a.error) : null,
|
|
608
|
+
// The loop lane's terminal signal, and the reason it is here: a re-direct that fails
|
|
609
|
+
// leaves `status` on `ready` (she is still serving the old loop, which is the whole
|
|
610
|
+
// design) and writes nothing to `error`. Without this field a caller polling after
|
|
611
|
+
// `setLoop` has NO way to distinguish "still rendering" from "gave up", and waits
|
|
612
|
+
// forever. queued → generating → ready | failed.
|
|
613
|
+
idleVideoStatus: a.idleVideoStatus ?? "none"
|
|
479
614
|
};
|
|
480
615
|
}
|
|
481
616
|
function toAsset(raw) {
|
package/dist/nextjs.d.ts
CHANGED
package/dist/nextjs.js
CHANGED
|
@@ -43,7 +43,7 @@ var RealtimeAvatarHttpError = class extends RealtimeAvatarError {
|
|
|
43
43
|
|
|
44
44
|
// ../http-client/src/client.ts
|
|
45
45
|
var DEFAULT_BASE_URL = "https://realtimeavatar.ai/api/v1";
|
|
46
|
-
var SDK_VERSION = "0.5.
|
|
46
|
+
var SDK_VERSION = "0.5.2";
|
|
47
47
|
var RealtimeAvatar = class {
|
|
48
48
|
#apiKey;
|
|
49
49
|
#baseUrl;
|
|
@@ -157,10 +157,15 @@ var RealtimeAvatar = class {
|
|
|
157
157
|
/**
|
|
158
158
|
* Register a character from a looping clip you host.
|
|
159
159
|
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
160
|
+
* @deprecated CLOSED to new callers — this answers `422` unless your tenant was already
|
|
161
|
+
* creating from video, in which case it keeps working and your existing avatars are
|
|
162
|
+
* untouched. Use {@link createAvatarFromImage}: one still, and the platform renders the
|
|
163
|
+
* resting loop and the motion library from it.
|
|
164
|
+
*
|
|
165
|
+
* The reason it closed is not arbitrary. Every clip has to start and end on ONE rest pose
|
|
166
|
+
* or a state switch reads as a jump, and the platform can only guarantee that when it
|
|
167
|
+
* rendered the loop and the clips from the same portrait. A supplied video cannot honour
|
|
168
|
+
* it, so the lane could never be made to look right.
|
|
164
169
|
*/
|
|
165
170
|
async createAvatarFromVideo(input) {
|
|
166
171
|
const asset = await this.createRemoteAsset({ kind: "video", remoteUrl: input.videoUrl });
|
|
@@ -173,12 +178,41 @@ var RealtimeAvatar = class {
|
|
|
173
178
|
metadata: input.metadata
|
|
174
179
|
});
|
|
175
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* Register a character from ONE still image. The platform generates everything moving:
|
|
183
|
+
* the resting loop she idles in, then a starter motion library rendered against her rest
|
|
184
|
+
* pose. No footage, no clip URLs, nothing to shoot.
|
|
185
|
+
*
|
|
186
|
+
* `motionPrompt` directs the RESTING LOOP — the video she plays when nothing else is
|
|
187
|
+
* happening — and it is the only chance to direct it, because there is no API today that
|
|
188
|
+
* re-generates a loop after creation (see {@link updateAvatar} for the one thing that can
|
|
189
|
+
* be re-pointed). Describe a small closed arc that returns to where it started: "settles
|
|
190
|
+
* into frame, breathes gently, a slow blink". Omit it and the house default is used.
|
|
191
|
+
*
|
|
192
|
+
* Creation returns while the avatar is still `preprocessing`; poll {@link getAvatar} until
|
|
193
|
+
* it leaves that state. The loop is load-bearing, so a failure there settles `failed` with
|
|
194
|
+
* a readable error — the motion library is not, and a library failure degrades to
|
|
195
|
+
* loop-only rather than demoting the character.
|
|
196
|
+
*/
|
|
197
|
+
async createAvatarFromImage(input) {
|
|
198
|
+
const asset = await this.createRemoteAsset({ kind: "image", remoteUrl: input.imageUrl });
|
|
199
|
+
return this.createAvatar({
|
|
200
|
+
displayName: input.displayName,
|
|
201
|
+
sourceKind: "image",
|
|
202
|
+
sourceAssetId: asset.id,
|
|
203
|
+
motionPrompt: input.motionPrompt,
|
|
204
|
+
voice: input.voice,
|
|
205
|
+
settings: input.settings,
|
|
206
|
+
metadata: input.metadata
|
|
207
|
+
});
|
|
208
|
+
}
|
|
176
209
|
async createAvatar(input) {
|
|
177
210
|
const body = {
|
|
178
211
|
displayName: input.displayName,
|
|
179
212
|
sourceKind: input.sourceKind,
|
|
180
213
|
sourceAssetId: input.sourceAssetId
|
|
181
214
|
};
|
|
215
|
+
if (input.motionPrompt !== void 0) body.motionPrompt = input.motionPrompt;
|
|
182
216
|
if (input.voice !== void 0) body.voice = input.voice;
|
|
183
217
|
if (input.settings !== void 0) body.settings = input.settings;
|
|
184
218
|
if (input.metadata !== void 0) body.metadata = input.metadata;
|
|
@@ -191,7 +225,14 @@ var RealtimeAvatar = class {
|
|
|
191
225
|
async getAvatar(avatarId) {
|
|
192
226
|
return toAvatar(await this.#json(await this.#request("GET", `/avatars/${avatarId}`)));
|
|
193
227
|
}
|
|
194
|
-
/**
|
|
228
|
+
/**
|
|
229
|
+
* Re-point what an avatar already is. `defaultVoiceId: null` clears the default voice.
|
|
230
|
+
*
|
|
231
|
+
* Cheap metadata only. The three things that cost a render have their own methods, because
|
|
232
|
+
* putting them in a patch alongside a rename would hide minutes of GPU work behind a field:
|
|
233
|
+
* {@link retimeAnchor} moves the rest frame, {@link swapSource} replaces the footage, and
|
|
234
|
+
* the resting loop is re-directed by description at `PUT /v1/avatars/{id}/loop`.
|
|
235
|
+
*/
|
|
195
236
|
async updateAvatar(avatarId, patch) {
|
|
196
237
|
return toAvatar(
|
|
197
238
|
await this.#json(await this.#request("PATCH", `/avatars/${avatarId}`, { json: patch }))
|
|
@@ -274,6 +315,94 @@ var RealtimeAvatar = class {
|
|
|
274
315
|
)
|
|
275
316
|
);
|
|
276
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* Re-direct the RESTING LOOP — the video she plays when nothing else is happening — from
|
|
320
|
+
* a new one-sentence description.
|
|
321
|
+
*
|
|
322
|
+
* Not a clip, and this is the distinction integrations get wrong: a clip with
|
|
323
|
+
* `role: "idle"` is a variant spliced OVER the loop, and declaring one never changes what
|
|
324
|
+
* she rests in. This is the only thing that does.
|
|
325
|
+
*
|
|
326
|
+
* `202`, because the render takes minutes. Three properties, all measured against a real
|
|
327
|
+
* render rather than asserted:
|
|
328
|
+
*
|
|
329
|
+
* - **She never goes dark.** She stays `ready` and keeps serving her previous loop for the
|
|
330
|
+
* entire render — returned as `servingUrl` — then the swap publishes in one step.
|
|
331
|
+
* - **Your clip library is untouched.** Clips render against the portrait, not against the
|
|
332
|
+
* loop, so a re-direct re-queues nothing and does not move `revision`.
|
|
333
|
+
* - **It bills once**, at the rendering model's rate, per re-direct.
|
|
334
|
+
*
|
|
335
|
+
* Refusals worth telling apart: `409 loop_pending` (one is already in flight — wait) and
|
|
336
|
+
* `422 loop_not_generatable` (a grandfathered video-sourced avatar has no portrait to
|
|
337
|
+
* re-animate — terminal, do not retry).
|
|
338
|
+
*/
|
|
339
|
+
async setLoop(avatarId, loop) {
|
|
340
|
+
return await this.#json(
|
|
341
|
+
await this.#request("PUT", `/avatars/${avatarId}/loop`, { json: { motionPrompt: loop.motionPrompt } })
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Block until a loop re-direct settles, and throw if it did not take.
|
|
346
|
+
*
|
|
347
|
+
* `setLoop` returns on ACCEPTANCE; the render runs for minutes afterwards. Every caller
|
|
348
|
+
* therefore writes the same polling loop, and the obvious version of it never terminates
|
|
349
|
+
* on failure — a failed re-direct leaves her `ready` (she is still serving the old loop)
|
|
350
|
+
* and writes nothing to `error`. `idleVideoStatus` is the only field that moves, which is
|
|
351
|
+
* why this exists rather than a doc line telling you to poll.
|
|
352
|
+
*
|
|
353
|
+
* Race-free without a baseline: the platform commits `queued` before `setLoop` returns, so
|
|
354
|
+
* by the time you can call this the status has already left `ready`.
|
|
355
|
+
*
|
|
356
|
+
* Resolves with the settled avatar — `sourceAssetId` is now the new loop. Throws on a
|
|
357
|
+
* failed render and on timeout; a timeout is not a failure, so re-poll or call again.
|
|
358
|
+
*/
|
|
359
|
+
async waitForLoop(avatarId, options = {}) {
|
|
360
|
+
const timeoutMs = options.timeoutMs ?? 20 * 6e4;
|
|
361
|
+
const pollMs = Math.max(1e3, options.pollMs ?? 1e4);
|
|
362
|
+
const deadline = Date.now() + timeoutMs;
|
|
363
|
+
for (; ; ) {
|
|
364
|
+
const avatar = await this.getAvatar(avatarId);
|
|
365
|
+
if (avatar.idleVideoStatus === "failed") {
|
|
366
|
+
throw new RealtimeAvatarError(
|
|
367
|
+
`Loop re-direct failed for ${avatarId}. She is still serving her previous loop \u2014 nothing was lost, and you can send another description.`
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
if (avatar.idleVideoStatus === "ready" || avatar.idleVideoStatus === "none") return avatar;
|
|
371
|
+
if (Date.now() >= deadline) {
|
|
372
|
+
throw new RealtimeAvatarError(
|
|
373
|
+
`Loop re-direct for ${avatarId} was still ${avatar.idleVideoStatus} after ${Math.round(timeoutMs / 1e3)}s. This is a timeout, not a failure \u2014 the render may still land.`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
await sleep(pollMs);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Block until every clip in the library has stopped moving.
|
|
381
|
+
*
|
|
382
|
+
* Settled means no row is `queued` or `generating` — NOT that every row is `ready`.
|
|
383
|
+
* Waiting for all-`ready` is the intuitive version and it hangs forever: a clip rejected
|
|
384
|
+
* by pose validation settles `failed`, which is terminal. So this returns the library
|
|
385
|
+
* with the failures in it and lets you decide; a partial library is a legitimate outcome
|
|
386
|
+
* and the rest of it is already serving.
|
|
387
|
+
*
|
|
388
|
+
* Throws only on timeout.
|
|
389
|
+
*/
|
|
390
|
+
async waitForClips(avatarId, options = {}) {
|
|
391
|
+
const timeoutMs = options.timeoutMs ?? 20 * 6e4;
|
|
392
|
+
const pollMs = Math.max(1e3, options.pollMs ?? 1e4);
|
|
393
|
+
const deadline = Date.now() + timeoutMs;
|
|
394
|
+
for (; ; ) {
|
|
395
|
+
const library = await this.listClips(avatarId);
|
|
396
|
+
const moving = library.data.filter((c) => c.status === "queued" || c.status === "generating");
|
|
397
|
+
if (moving.length === 0) return library;
|
|
398
|
+
if (Date.now() >= deadline) {
|
|
399
|
+
throw new RealtimeAvatarError(
|
|
400
|
+
`${moving.length} clip(s) on ${avatarId} were still rendering after ${Math.round(timeoutMs / 1e3)}s: ${moving.map((c) => c.clipId).join(", ")}. This is a timeout, not a failure.`
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
await sleep(pollMs);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
277
406
|
/** The avatar's clip library: every non-retired clip, plus revision, anchor and eligibility. */
|
|
278
407
|
async listClips(avatarId) {
|
|
279
408
|
return this.#clipEnvelope(
|
|
@@ -475,7 +604,13 @@ function toAvatar(raw) {
|
|
|
475
604
|
sourceAssetId: a.sourceAssetId ? String(a.sourceAssetId) : null,
|
|
476
605
|
// Carried because it is the ONLY channel a failed source swap has: she stays `ready`
|
|
477
606
|
// and serving, and this says why the re-shoot did not take.
|
|
478
|
-
error: a.error ? String(a.error) : null
|
|
607
|
+
error: a.error ? String(a.error) : null,
|
|
608
|
+
// The loop lane's terminal signal, and the reason it is here: a re-direct that fails
|
|
609
|
+
// leaves `status` on `ready` (she is still serving the old loop, which is the whole
|
|
610
|
+
// design) and writes nothing to `error`. Without this field a caller polling after
|
|
611
|
+
// `setLoop` has NO way to distinguish "still rendering" from "gave up", and waits
|
|
612
|
+
// forever. queued → generating → ready | failed.
|
|
613
|
+
idleVideoStatus: a.idleVideoStatus ?? "none"
|
|
479
614
|
};
|
|
480
615
|
}
|
|
481
616
|
function toAsset(raw) {
|
|
@@ -487,6 +487,28 @@ type AvatarVideoSurfaceProps = {
|
|
|
487
487
|
children?: ReactNode;
|
|
488
488
|
/** Surface a small "live · WxH" badge when the live layer is shown. Default true. */
|
|
489
489
|
showLiveBadge?: boolean;
|
|
490
|
+
/**
|
|
491
|
+
* Reclaim the flat 0.5s de-jitter cushion on clean networks: a 1Hz closed loop
|
|
492
|
+
* over `getStats()` that descends toward a 150ms floor while jitter and loss stay
|
|
493
|
+
* low, and snaps back to 0.5s within a couple of ticks when either appears.
|
|
494
|
+
* Default **false** — the flat
|
|
495
|
+
* cushion is the shipped behavior and this is the opt-in that trades loss margin
|
|
496
|
+
* for up to ~350ms of felt reply latency. The loop always OPENS at the ceiling, so
|
|
497
|
+
* enabling it can never make the first frame later than it is today.
|
|
498
|
+
*/
|
|
499
|
+
adaptivePlayout?: boolean;
|
|
500
|
+
/**
|
|
501
|
+
* Called with the receiver playout depth (SECONDS) whenever it changes — the flat
|
|
502
|
+
* {@link DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS} while `adaptivePlayout` is off, the live
|
|
503
|
+
* value while it is on.
|
|
504
|
+
*
|
|
505
|
+
* Read this if anything you render is timed against the avatar's VOICE: the media rides
|
|
506
|
+
* the receiver buffer and a side channel (captions, a transcript reveal, a lip-synced
|
|
507
|
+
* overlay) does not, so it has to be held by the same amount or it arrives early. While
|
|
508
|
+
* the cushion was a constant, consumers hard-coded it; once it moves, a hard-coded copy
|
|
509
|
+
* desyncs by up to the whole adaptive range. Fires on change only, not per frame.
|
|
510
|
+
*/
|
|
511
|
+
onPlayoutDelayChange?: (seconds: number) => void;
|
|
490
512
|
/** Test id for the box. */
|
|
491
513
|
"data-testid"?: string;
|
|
492
514
|
};
|
|
@@ -845,6 +867,130 @@ type RealtimeAvatarLiveKitRoomProps = Omit<LiveKitRoomProps, "serverUrl" | "toke
|
|
|
845
867
|
declare function RealtimeAvatarLiveKitRoom(props: RealtimeAvatarLiveKitRoomProps): ReactElement;
|
|
846
868
|
declare const capacityErrorFromBusy: (busy: CapacityBusyResponse) => RealtimeAvatarCapacityError;
|
|
847
869
|
|
|
870
|
+
/**
|
|
871
|
+
* The adaptive playout-delay controller — the closed loop behind
|
|
872
|
+
* `useAvatarAdaptivePlayoutDelay`.
|
|
873
|
+
*
|
|
874
|
+
* The flat {@link DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS} cushion exists for loss
|
|
875
|
+
* robustness (measured at ~5% loss it takes the stream from ~11fps with
|
|
876
|
+
* multi-second freezes to a steady 25fps) — but it bills every viewer 0.5s of
|
|
877
|
+
* felt reply latency on every turn, clean network or not. This controller keeps
|
|
878
|
+
* the cushion where the network needs it and reclaims it where it doesn't:
|
|
879
|
+
*
|
|
880
|
+
* target = clamp(floor + jitterGain·EWMA(jitter) + lossGain·EWMA(loss),
|
|
881
|
+
* floor, ceiling)
|
|
882
|
+
*
|
|
883
|
+
* with three deliberate asymmetries, each a lesson already paid for:
|
|
884
|
+
*
|
|
885
|
+
* 1. **It OPENS at the ceiling** (the incumbent 0.5s) and only ever descends in
|
|
886
|
+
* steady state. The open-small-then-ramp variant shipped, failed twice (the
|
|
887
|
+
* rVFC ramp trigger is throttled on an opacity:0-mounted element, stranding
|
|
888
|
+
* the shallow buffer), and was removed in 2262e4c7b. This loop never opens
|
|
889
|
+
* shallow and never depends on rVFC — its clock is a plain interval over
|
|
890
|
+
* `RTCRtpReceiver.getStats()`.
|
|
891
|
+
* 2. **Grow fast, shrink slow.** A loss burst must deepen the buffer within a
|
|
892
|
+
* couple of ticks (freezes are the cardinal sin); a clean stretch earns
|
|
893
|
+
* latency back over tens of seconds. In the continuous-render model the
|
|
894
|
+
* session is long, so there is plenty of steady state to reclaim in.
|
|
895
|
+
* 3. **Hysteresis.** The hint is only re-applied when the smoothed target moved
|
|
896
|
+
* by more than `hysteresisSeconds` — a receiver being re-hinted every second
|
|
897
|
+
* with ±5ms wiggle re-anchors its buffer for nothing.
|
|
898
|
+
*
|
|
899
|
+
* Pure by construction: no react, no livekit, no DOM — a plain state machine
|
|
900
|
+
* over numbers, imported directly by the node test runner. The hook that feeds
|
|
901
|
+
* it stats lives in `use-adaptive-playout.ts`.
|
|
902
|
+
*/
|
|
903
|
+
/** One tick's network observation, already reduced to the two axes that matter. */
|
|
904
|
+
interface AdaptivePlayoutSample {
|
|
905
|
+
/** RTP jitter in SECONDS (`inbound-rtp.jitter`), worst of the avatar's receivers. */
|
|
906
|
+
jitterSeconds: number;
|
|
907
|
+
/** Fraction of packets lost over the tick (0..1), worst of the avatar's receivers. */
|
|
908
|
+
lossFraction: number;
|
|
909
|
+
}
|
|
910
|
+
interface AdaptivePlayoutOptions {
|
|
911
|
+
/** The clean-network resting depth. Default 0.15s — the design's floor. */
|
|
912
|
+
floorSeconds?: number;
|
|
913
|
+
/** The lossy-network depth AND the opening value. Default 0.5s (the incumbent flat cushion). */
|
|
914
|
+
ceilingSeconds?: number;
|
|
915
|
+
/** Seconds of buffer per second of RTP jitter. Default 4 (25ms jitter ⇒ +100ms). */
|
|
916
|
+
jitterGain?: number;
|
|
917
|
+
/** Seconds of buffer per unit loss fraction. Default 5 (5% loss ⇒ +250ms ⇒ near ceiling). */
|
|
918
|
+
lossGain?: number;
|
|
919
|
+
/** EWMA blend when the demand RISES. Default 0.6 — most of a burst lands in 2 ticks. */
|
|
920
|
+
growAlpha?: number;
|
|
921
|
+
/** EWMA blend when the demand FALLS. Default 0.08 — ~9-tick half-life, latency earned back slowly. */
|
|
922
|
+
shrinkAlpha?: number;
|
|
923
|
+
/** Minimum movement before the hint is re-applied. Default 0.05s (the design's 50ms). */
|
|
924
|
+
hysteresisSeconds?: number;
|
|
925
|
+
}
|
|
926
|
+
interface AdaptivePlayoutDecision {
|
|
927
|
+
/** The depth the receivers should sit at, seconds. Always within [floor, ceiling]. */
|
|
928
|
+
targetSeconds: number;
|
|
929
|
+
/** True when the caller should re-apply the hint (movement cleared the hysteresis). */
|
|
930
|
+
changed: boolean;
|
|
931
|
+
}
|
|
932
|
+
declare class AdaptivePlayoutController {
|
|
933
|
+
private readonly opts;
|
|
934
|
+
private smoothed;
|
|
935
|
+
private applied;
|
|
936
|
+
constructor(options?: AdaptivePlayoutOptions);
|
|
937
|
+
/** The currently applied depth (what the receivers were last hinted to). */
|
|
938
|
+
get appliedSeconds(): number;
|
|
939
|
+
update(sample: AdaptivePlayoutSample): AdaptivePlayoutDecision;
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Cumulative counters from one receiver's previous `inbound-rtp` report, kept by
|
|
943
|
+
* the caller so loss can be computed as a PER-TICK delta (the stats counters are
|
|
944
|
+
* cumulative for the whole session — a session-long ratio would never recover
|
|
945
|
+
* from one bad patch).
|
|
946
|
+
*/
|
|
947
|
+
interface InboundRtpCursor {
|
|
948
|
+
packetsLost: number;
|
|
949
|
+
packetsReceived: number;
|
|
950
|
+
}
|
|
951
|
+
interface InboundRtpReading {
|
|
952
|
+
sample: AdaptivePlayoutSample;
|
|
953
|
+
cursor: InboundRtpCursor;
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Reduce one receiver's `getStats()` report to this tick's {@link AdaptivePlayoutSample}.
|
|
957
|
+
* Takes the report as a plain iterable of stat dicts so tests need no RTCStatsReport.
|
|
958
|
+
* Returns undefined when the report carries no `inbound-rtp` entry (nothing decodable
|
|
959
|
+
* yet) — the caller should skip the tick rather than feed a fabricated zero.
|
|
960
|
+
*
|
|
961
|
+
* Counter resets (a receiver restart makes cumulative counters go BACKWARD) read as
|
|
962
|
+
* a zero-loss tick, never a negative one.
|
|
963
|
+
*/
|
|
964
|
+
declare function readInboundRtp(reports: Iterable<Record<string, unknown>>, previous?: InboundRtpCursor): InboundRtpReading | undefined;
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* The adaptive counterpart to `useAvatarPlayoutDelay` — OPT-IN via `enabled`
|
|
968
|
+
* (default false ⇒ a complete no-op, the incumbent flat cushion untouched).
|
|
969
|
+
*
|
|
970
|
+
* When enabled it runs the {@link AdaptivePlayoutController} closed loop: a 1Hz
|
|
971
|
+
* `RTCRtpReceiver.getStats()` poll over BOTH avatar receivers, the worst
|
|
972
|
+
* jitter/loss of the pair feeding one shared target so audio and video stay on
|
|
973
|
+
* the SAME depth (lip-sync law — WebRTC pairs the streams to the larger of the
|
|
974
|
+
* two hints). The loop opens at the ceiling (the flat cushion already applied by
|
|
975
|
+
* `useAvatarPlayoutDelay`), descends toward the 150ms floor on clean paths, and
|
|
976
|
+
* snaps back up within a couple of ticks when jitter or loss appears.
|
|
977
|
+
*
|
|
978
|
+
* Degrades to the incumbent behavior wherever the substrate is missing: a track
|
|
979
|
+
* without `getStats` on its receiver, a stats report with no `inbound-rtp` yet,
|
|
980
|
+
* or a browser without the playout-delay hint all leave the flat cushion exactly
|
|
981
|
+
* as `useAvatarPlayoutDelay` set it.
|
|
982
|
+
*
|
|
983
|
+
* RETURNS THE APPLIED DEPTH IN SECONDS, and that return value is load-bearing rather
|
|
984
|
+
* than a convenience. Anything a consumer times against the avatar's voice — a caption
|
|
985
|
+
* reveal being the real case — has to be held by the SAME cushion, because the media is
|
|
986
|
+
* buffered and a side channel is not. While the cushion was a flat constant a consumer
|
|
987
|
+
* could hard-code it; the moment it moves, a hard-coded copy desyncs by up to the whole
|
|
988
|
+
* adaptive range. So the value comes back out of the hook, and stays equal to
|
|
989
|
+
* {@link DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS} whenever the loop is disabled or inert —
|
|
990
|
+
* a consumer can read it unconditionally.
|
|
991
|
+
*/
|
|
992
|
+
declare function useAvatarAdaptivePlayoutDelay(videoTrack: TrackReferenceOrPlaceholder | undefined, audioTrack: TrackReferenceOrPlaceholder | undefined, enabled?: boolean, options?: AdaptivePlayoutOptions): number;
|
|
993
|
+
|
|
848
994
|
/**
|
|
849
995
|
* Avatar nonverbal-behavior snapshot + its SSOT derivation.
|
|
850
996
|
*
|
|
@@ -1473,4 +1619,4 @@ interface ProxyClientOptions {
|
|
|
1473
1619
|
}
|
|
1474
1620
|
declare function createProxyClient(options: ProxyClientOptions): AvatarSessionClient;
|
|
1475
1621
|
|
|
1476
|
-
export {
|
|
1622
|
+
export { MAX_SESSION_INSTRUCTIONS_CHARS as $, type AvatarSessionClient as A, type BehaviorSnapshot as B, type CallTranscript as C, DEFAULT_APPROACHING_END_LEAD_SECONDS as D, type EndReason as E, type FishTtsModel as F, type Governor as G, type GovernorConfig as H, type GovernorSignal as I, type GovernorState as J, type GraceWindowClosedEvent as K, type GraceWindowOpenEvent as L, type GraceWindowState as M, type IdleWarningEvent as N, type InboundRtpCursor as O, type InboundRtpReading as P, type KnownBehaviorState as Q, type LLMProvider as R, type LLMSelection as S, type LiveKitAvatarGrantState as T, type LiveKitAvatarGrantStatus as U, type LiveKitCapacityState as V, type LiveKitConnectionStatus as W, type LiveKitSessionRequest as X, type LiveKitSessionStartResult as Y, type LivePlaybackKeeper as Z, MAX_RECONNECT_ATTEMPTS as _, type AvatarVideoFit as a, MIC_LEASE_ENDED_TIMEOUT_MS as a0, type MicLease as a1, type PlayableVideoElement as a2, type ProxyClientOptions as a3, type QualityCap as a4, RECONNECT_BACKOFF_MS as a5, RealtimeAvatarCapacityError as a6, RealtimeAvatarLiveKitRoom as a7, type RealtimeAvatarLiveKitRoomProps as a8, type RealtimeAvatarRequestOptions as a9, capacityStateFromGrant as aA, createProxyClient as aB, knownBehaviorStates as aC, mapTurnState as aD, readInboundRtp as aE, sessionBehaviorSchema as aF, sessionClipSchema as aG, shouldReplayPendingTurn as aH, splitCallTranscript as aI, useAvatarAdaptivePlayoutDelay as aJ, useAvatarPlayoutDelay as aK, useAvatarQualityGovernor as aL, useCallTranscript as aM, useLiveKitAvatarGrant as aN, useMicLease as aO, useRealtimeSession as aP, useReleaseMicLeaseOnTrackEnded as aQ, useSessionLifecycle as aR, type LiveKitSessionGrant as aS, isNativeLiveTrackSubscribed as aT, useLiveTrackProducing as aU, type RealtimeSessionApi as aa, type RealtimeSessionMedia as ab, type RealtimeSessionRoomSinks as ac, type ReconnectPolicy as ad, type ReconnectingEvent as ae, type RecoveryState as af, type RetryStep as ag, type SessionBehavior as ah, type SessionClip as ai, type SessionClocks as aj, type SessionEndReason as ak, type SessionLifecycleApi as al, type SessionLifecyclePhase as am, type SessionLifecyclePhaseKind as an, SessionLifecycleRoomBridge as ao, type SessionLifecycleRoomBridgeProps as ap, type SurfaceLayers as aq, type TurnState as ar, type TurnTimeoutEvent as as, type UseAvatarQualityGovernorInput as at, type UseLiveKitAvatarGrantInput as au, type UseRealtimeSessionInput as av, type UseSessionLifecycleInput as aw, type VoiceSpec as ax, type VoiceSpecInput as ay, capacityErrorFromBusy as az, AdaptivePlayoutController as b, type AdaptivePlayoutDecision as c, type AdaptivePlayoutOptions as d, type AdaptivePlayoutSample as e, type ApproachingEndEvent as f, type ApproachingEndReason as g, AvatarVideoSurface as h, type AvatarVideoSurfaceProps as i, type CallTranscriptSegment as j, type CartesiaTtsModel as k, type ClipResult as l, type ClosingTurnResult as m, type CreditsLowEvent as n, DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS as o, DEFAULT_CREDITS_LOW_LEAD_SECONDS as p, DEFAULT_GOVERNOR_CONFIG as q, DEFAULT_GRACE_CEILING_SECONDS as r, DEFAULT_GRACE_WINDOW_LEAD_SECONDS as s, DEFAULT_IDLE_SECONDS as t, DEFAULT_IDLE_WARN_LEAD_SECONDS as u, DEFAULT_TURN_TIMEOUT_SECONDS as v, type EndedEvent as w, type ExtendResult as x, type FreezeReadingFn as y, type GovernorAction as z };
|
package/dist/react-native.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { LiveKitRoomProps } from '@livekit/react-native';
|
|
2
2
|
export { AudioSession, VideoTrack, VideoTrackProps, registerGlobals } from '@livekit/react-native';
|
|
3
3
|
import { ReactNode, ReactElement } from 'react';
|
|
4
|
-
import {
|
|
5
|
-
export { b as
|
|
4
|
+
import { aS as LiveKitSessionGrant, a as AvatarVideoFit } from './proxy-client-BIaUo0az.js';
|
|
5
|
+
export { b as AdaptivePlayoutController, c as AdaptivePlayoutDecision, d as AdaptivePlayoutOptions, e as AdaptivePlayoutSample, f as ApproachingEndEvent, g as ApproachingEndReason, A as AvatarSessionClient, B as BehaviorSnapshot, C as CallTranscript, j as CallTranscriptSegment, k as CartesiaTtsModel, l as ClipResult, m as ClosingTurnResult, n as CreditsLowEvent, D as DEFAULT_APPROACHING_END_LEAD_SECONDS, o as DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS, p as DEFAULT_CREDITS_LOW_LEAD_SECONDS, q as DEFAULT_GOVERNOR_CONFIG, r as DEFAULT_GRACE_CEILING_SECONDS, s as DEFAULT_GRACE_WINDOW_LEAD_SECONDS, t as DEFAULT_IDLE_SECONDS, u as DEFAULT_IDLE_WARN_LEAD_SECONDS, v as DEFAULT_TURN_TIMEOUT_SECONDS, E as EndReason, w as EndedEvent, x as ExtendResult, F as FishTtsModel, y as FreezeReadingFn, G as Governor, z as GovernorAction, H as GovernorConfig, I as GovernorSignal, J as GovernorState, K as GraceWindowClosedEvent, L as GraceWindowOpenEvent, M as GraceWindowState, N as IdleWarningEvent, O as InboundRtpCursor, P as InboundRtpReading, Q as KnownBehaviorState, R as LLMProvider, S as LLMSelection, T as LiveKitAvatarGrantState, U as LiveKitAvatarGrantStatus, V as LiveKitCapacityState, W as LiveKitConnectionStatus, X as LiveKitSessionRequest, Y as LiveKitSessionStartResult, _ as MAX_RECONNECT_ATTEMPTS, $ as MAX_SESSION_INSTRUCTIONS_CHARS, a3 as ProxyClientOptions, a4 as QualityCap, a5 as RECONNECT_BACKOFF_MS, a9 as RealtimeAvatarRequestOptions, aa as RealtimeSessionApi, ab as RealtimeSessionMedia, ac as RealtimeSessionRoomSinks, ad as ReconnectPolicy, ae as ReconnectingEvent, af as RecoveryState, ag as RetryStep, ah as SessionBehavior, ai as SessionClip, aj as SessionClocks, ak as SessionEndReason, al as SessionLifecycleApi, am as SessionLifecyclePhase, an as SessionLifecyclePhaseKind, ao as SessionLifecycleRoomBridge, ap as SessionLifecycleRoomBridgeProps, aq as SurfaceLayers, ar as TurnState, as as TurnTimeoutEvent, at as UseAvatarQualityGovernorInput, au as UseLiveKitAvatarGrantInput, av as UseRealtimeSessionInput, aw as UseSessionLifecycleInput, ax as VoiceSpec, ay as VoiceSpecInput, az as capacityErrorFromBusy, aA as capacityStateFromGrant, aB as createProxyClient, aT as isNativeLiveTrackSubscribed, aC as knownBehaviorStates, aD as mapTurnState, aE as readInboundRtp, aF as sessionBehaviorSchema, aG as sessionClipSchema, aI as splitCallTranscript, aJ as useAvatarAdaptivePlayoutDelay, aK as useAvatarPlayoutDelay, aL as useAvatarQualityGovernor, aM as useCallTranscript, aN as useLiveKitAvatarGrant, aU as useLiveTrackProducing, aO as useMicLease, aP as useRealtimeSession, aQ as useReleaseMicLeaseOnTrackEnded, aR as useSessionLifecycle } from './proxy-client-BIaUo0az.js';
|
|
6
6
|
import { StyleProp, ViewStyle } from 'react-native';
|
|
7
7
|
export { useChat, useConnectionState, useLocalParticipant, useRoomContext, useTrackToggle, useTranscriptions, useVoiceAssistant } from '@livekit/components-react';
|
|
8
8
|
export { DisconnectReason, RemoteAudioTrack, RemoteTrack, RemoteVideoTrack, Room, RoomEvent, SendTextOptions, Track } from 'livekit-client';
|
|
@@ -88,6 +88,14 @@ type AvatarVideoSurfaceProps = {
|
|
|
88
88
|
children?: ReactNode;
|
|
89
89
|
/** Surface a small "live · WxH" badge when the live layer is shown. Default true. */
|
|
90
90
|
showLiveBadge?: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Reclaim the flat 0.5s de-jitter cushion on clean networks — the same opt-in
|
|
93
|
+
* closed loop the web surface takes, sharing the same implementation. Default
|
|
94
|
+
* **false**. The loop is stats-driven (no rVFC), so it behaves identically on
|
|
95
|
+
* iOS and Android; a receiver whose WebRTC shim exposes no `getStats` simply
|
|
96
|
+
* keeps the flat cushion.
|
|
97
|
+
*/
|
|
98
|
+
adaptivePlayout?: boolean;
|
|
91
99
|
/** Test id for the box. */
|
|
92
100
|
testID?: string;
|
|
93
101
|
};
|