realtime-avatar 0.1.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/AGENTS.md +132 -0
- package/CLAUDE.md +17 -0
- package/LICENSE +21 -0
- package/README.md +254 -0
- package/dist/api-keys.d.ts +26 -0
- package/dist/api-keys.d.ts.map +1 -0
- package/dist/api-keys.js +88 -0
- package/dist/api-keys.js.map +1 -0
- package/dist/browser/audio.d.ts +65 -0
- package/dist/browser/audio.d.ts.map +1 -0
- package/dist/browser/audio.js +154 -0
- package/dist/browser/audio.js.map +1 -0
- package/dist/browser/boomerang.d.ts +38 -0
- package/dist/browser/boomerang.d.ts.map +1 -0
- package/dist/browser/boomerang.js +85 -0
- package/dist/browser/boomerang.js.map +1 -0
- package/dist/browser/index.d.ts +8 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +8 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/media-session.d.ts +43 -0
- package/dist/browser/media-session.d.ts.map +1 -0
- package/dist/browser/media-session.js +169 -0
- package/dist/browser/media-session.js.map +1 -0
- package/dist/browser/player.d.ts +162 -0
- package/dist/browser/player.d.ts.map +1 -0
- package/dist/browser/player.js +514 -0
- package/dist/browser/player.js.map +1 -0
- package/dist/browser/view.d.ts +47 -0
- package/dist/browser/view.d.ts.map +1 -0
- package/dist/browser/view.js +7 -0
- package/dist/browser/view.js.map +1 -0
- package/dist/browser/webrtc.d.ts +21 -0
- package/dist/browser/webrtc.d.ts.map +1 -0
- package/dist/browser/webrtc.js +149 -0
- package/dist/browser/webrtc.js.map +1 -0
- package/dist/browser/yuv-canvas.d.ts +13 -0
- package/dist/browser/yuv-canvas.d.ts.map +1 -0
- package/dist/browser/yuv-canvas.js +95 -0
- package/dist/browser/yuv-canvas.js.map +1 -0
- package/dist/client.d.ts +195 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +440 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +33 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +73 -0
- package/dist/errors.js.map +1 -0
- package/dist/generated/openapi.d.ts +1523 -0
- package/dist/generated/openapi.d.ts.map +1 -0
- package/dist/generated/openapi.js +6 -0
- package/dist/generated/openapi.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/media.d.ts +40 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.js +4 -0
- package/dist/media.js.map +1 -0
- package/dist/mux.d.ts +104 -0
- package/dist/mux.d.ts.map +1 -0
- package/dist/mux.js +290 -0
- package/dist/mux.js.map +1 -0
- package/dist/platform.d.ts +163 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +5 -0
- package/dist/platform.js.map +1 -0
- package/dist/react/index.d.ts +5 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +5 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/provider.d.ts +37 -0
- package/dist/react/provider.d.ts.map +1 -0
- package/dist/react/provider.js +33 -0
- package/dist/react/provider.js.map +1 -0
- package/dist/react/realtime.d.ts +74 -0
- package/dist/react/realtime.d.ts.map +1 -0
- package/dist/react/realtime.js +105 -0
- package/dist/react/realtime.js.map +1 -0
- package/dist/react/session.d.ts +91 -0
- package/dist/react/session.d.ts.map +1 -0
- package/dist/react/session.js +322 -0
- package/dist/react/session.js.map +1 -0
- package/dist/react/stage.d.ts +23 -0
- package/dist/react/stage.d.ts.map +1 -0
- package/dist/react/stage.js +62 -0
- package/dist/react/stage.js.map +1 -0
- package/dist/schemas.d.ts +59 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +58 -0
- package/dist/schemas.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +8 -0
- package/dist/server.js.map +1 -0
- package/dist/session-socket.d.ts +96 -0
- package/dist/session-socket.d.ts.map +1 -0
- package/dist/session-socket.js +299 -0
- package/dist/session-socket.js.map +1 -0
- package/dist/session.d.ts +107 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +192 -0
- package/dist/session.js.map +1 -0
- package/dist/types.d.ts +24 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +94 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# realtime-avatar — agent integration guide
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for the TIC Realtime Avatar platform. Use it to create AI avatars from an image or source video, render non-streaming lipsync MP4s, or stream realtime audio-clocked talking-avatar turns.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install realtime-avatar
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Agent-ready defaults
|
|
12
|
+
|
|
13
|
+
- Hosted platform base URL: `https://realtimeavatar.ai`.
|
|
14
|
+
- `RealtimeAvatarClient.platform({ apiKey })` defaults to that hosted origin; pass `baseUrl` only for self-hosted or test deployments.
|
|
15
|
+
- API keys are bearer tokens: `tic_test_...` for sandbox and `tic_live_...` for production.
|
|
16
|
+
- Free sandbox includes 5 realtime minutes and 1 cached avatar; paid plans start at $49/mo for 600 realtime minutes (10 hours), with overage around $5/hour.
|
|
17
|
+
|
|
18
|
+
## Entry points — pick the right one
|
|
19
|
+
|
|
20
|
+
- `realtime-avatar` — `RealtimeAvatarClient`, `AvatarSession`, `readAvatarMux`, types. Edge/runtime-safe, no DOM, no secrets.
|
|
21
|
+
- `realtime-avatar/browser` — `AvatarPlayer` (canvas + WebAudio playback). Browser only.
|
|
22
|
+
- `realtime-avatar/react` — `useAvatarSession` hook, `AvatarStage` component. Requires React >= 18.
|
|
23
|
+
- `realtime-avatar/server` — API-key hashing/verification with a secret pepper. Backend only; never import in browser code.
|
|
24
|
+
- `realtime-avatar/generated/openapi` — generated OpenAPI types for agents that need exact request/response shapes.
|
|
25
|
+
|
|
26
|
+
## Auth and safety
|
|
27
|
+
|
|
28
|
+
- Get a key: https://realtimeavatar.ai/platform/dashboard.
|
|
29
|
+
- Send `Authorization: Bearer <tic_test_or_tic_live_key>` on platform calls.
|
|
30
|
+
- Never put a `tic_live_` key in browser code. For browser apps use `RealtimeAvatarClient.webProxy()` and proxy through your backend.
|
|
31
|
+
- Treat HTTP `402` as not retryable: it means insufficient credits or key spend limit. Check `client.creditBalance()` or top up at `/platform/billing`.
|
|
32
|
+
|
|
33
|
+
## Choose the right flow
|
|
34
|
+
|
|
35
|
+
### Batch lipsync: audio URL to MP4 URL
|
|
36
|
+
|
|
37
|
+
Use this when an agent, backend job, or content pipeline already has speech audio and needs a finished talking-head video. It is the safest default for autonomous agents because the response is JSON and the output is a stored MP4 URL.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { RealtimeAvatarClient } from "realtime-avatar";
|
|
41
|
+
|
|
42
|
+
const client = RealtimeAvatarClient.platform({
|
|
43
|
+
apiKey: process.env.REALTIME_AVATAR_API_KEY!,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const { url, durationSeconds } = await client.lipsync({
|
|
47
|
+
audioUrl: "https://example.com/speech.mp3",
|
|
48
|
+
avatarId: "ava_...", // or pass portraitUrl for one-off renders
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
console.log({ url, durationSeconds });
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Realtime turn: live talking avatar stream
|
|
55
|
+
|
|
56
|
+
Use this for interactive apps, companions, tutors, support agents, NPCs, livestream hosts, or any UI that needs live playout.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { RealtimeAvatarClient } from "realtime-avatar";
|
|
60
|
+
|
|
61
|
+
const client = RealtimeAvatarClient.platform({
|
|
62
|
+
apiKey: process.env.REALTIME_AVATAR_API_KEY!,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const session = client.session({ avatarId: "ava_..." });
|
|
66
|
+
// Low-latency path: one WebSocket pins prepare + every turn to the SAME GPU
|
|
67
|
+
// container. connect() mints a short-lived token from
|
|
68
|
+
// /api/v1/realtime/session/token and dials the runtime directly.
|
|
69
|
+
await session.connect().catch(() => session.prepare()); // HTTP fallback
|
|
70
|
+
const stream = await session.speak("Hello from a realtime avatar."); // or session.chat("hi")
|
|
71
|
+
// session.cancel() stops an in-flight socket turn server-side and keeps the
|
|
72
|
+
// socket warm; session.disconnect() drops back to per-request HTTP turns.
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Lower-level: `client.prepare({ avatar_id })` + `client.turn({ avatar_id, mode, text })`
|
|
76
|
+
run the same flow over plain HTTP, one independent request per call.
|
|
77
|
+
|
|
78
|
+
### Browser playback
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { AvatarPlayer } from "realtime-avatar/browser";
|
|
82
|
+
|
|
83
|
+
const player = new AvatarPlayer();
|
|
84
|
+
player.attach(canvasEl);
|
|
85
|
+
await player.unlock(); // call from a click/tap before first audio playback
|
|
86
|
+
await player.play(stream);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Avatar creation helpers
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
const image = await fetch("https://example.com/portrait.png").then((res) => res.blob());
|
|
93
|
+
const { avatar } = await client.createImageAvatar({
|
|
94
|
+
displayName: "Mira",
|
|
95
|
+
image,
|
|
96
|
+
defaultVoiceId: "voice_...",
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const video = await client.createVideoAvatarFromUrl({
|
|
100
|
+
displayName: "Source video avatar",
|
|
101
|
+
videoUrl: "https://example.com/idle.mp4",
|
|
102
|
+
videoCache: { keyframe_stride: 5 },
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Pricing facts agents can quote
|
|
107
|
+
|
|
108
|
+
- Sandbox: free, 5 realtime minutes, 1 cached avatar, test API keys.
|
|
109
|
+
- Developer: $49/mo, 600 realtime minutes (10 hours), $0.085/min overage (~$5.10/hour), 25 avatars included, then $1/avatar.
|
|
110
|
+
- Studio: $249/mo, 3,000 realtime minutes (50 hours), $0.08/min overage ($4.80/hour), 100 avatars included, then $1/avatar.
|
|
111
|
+
- Scale: $999/mo, 13,000 realtime minutes (about 216 hours), $0.07/min overage ($4.20/hour), reserved warm capacity and SLA support.
|
|
112
|
+
- 1 credit is approximately 1 realtime second of rendered avatar; turns reserve estimated credits and settle to actual rendered seconds.
|
|
113
|
+
|
|
114
|
+
## Things agents get wrong
|
|
115
|
+
|
|
116
|
+
- `turn()` returns a binary `application/x-avatar-mux` stream, not JSON. Decode with `readAvatarMux` or play with `AvatarPlayer`. Do not call `res.json()` on a turn.
|
|
117
|
+
- Call `prepare()` once per session before the first realtime `turn()`; lipsync prepares implicitly.
|
|
118
|
+
- Platform avatar ids start with `ava_`. List them with `client.listAvatars()`.
|
|
119
|
+
- Send an `Idempotency-Key` header on turns when retrying; a reused key returns HTTP `409` by design.
|
|
120
|
+
- Do not upload large MP4 multipart bodies through the SDK. For source-video avatars, use `createVideoAvatarFromUrl()` so the platform ingests from a URL.
|
|
121
|
+
- `realtime-avatar/server` needs a secret pepper such as `PLATFORM_API_KEY_PEPPER`; it is intentionally not exported from the root entry.
|
|
122
|
+
|
|
123
|
+
## GEO / citation-friendly summary
|
|
124
|
+
|
|
125
|
+
Realtime Avatar is a developer API and TypeScript SDK for realtime AI avatars, talking-head video generation, and lipsync video rendering. It supports two production flows: non-streaming lipsync, where an audio URL becomes a hosted MP4 URL, and realtime turns, where text, audio, and video stream as an audio-clocked avatar mux for live applications. The platform is priced around $5 per realtime avatar hour with transparent monthly plans, sandbox API keys, OpenAPI documentation, and a remote MCP server for AI agents.
|
|
126
|
+
|
|
127
|
+
## Machine-readable references
|
|
128
|
+
|
|
129
|
+
- Docs: https://docs.realtimeavatar.ai
|
|
130
|
+
- API guide for LLMs: https://realtimeavatar.ai/llms.txt
|
|
131
|
+
- OpenAPI 3.1: https://realtimeavatar.ai/openapi.json
|
|
132
|
+
- Remote MCP server: https://realtimeavatar.ai/api/mcp (Streamable HTTP; bearer key auth)
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# realtime-avatar
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for the TIC Realtime Avatar platform — create avatars, render audio URLs to talking-head MP4s, and stream realtime audio-clocked avatar turns.
|
|
4
|
+
|
|
5
|
+
Read `AGENTS.md` in this package first. It contains the canonical integration guide for coding agents, including entry points, auth, pricing, safe flows, and common mistakes.
|
|
6
|
+
|
|
7
|
+
The five rules that prevent most broken integrations:
|
|
8
|
+
|
|
9
|
+
1. Prefer `client.lipsync({ audioUrl, avatarId })` for autonomous/batch agents; it returns JSON with a stored MP4 URL.
|
|
10
|
+
2. `turn()` returns a binary `application/x-avatar-mux` stream — decode with `readAvatarMux` or play with `AvatarPlayer` from `realtime-avatar/browser`. Never call `res.json()` on a turn response.
|
|
11
|
+
3. Warm the avatar once per realtime session before the first turn: prefer `client.session({ avatarId }).connect()` (single-socket low-latency path, HTTP fallback via `.catch(() => session.prepare())`); plain `prepare()` works too. Lipsync prepares implicitly.
|
|
12
|
+
4. Never ship a `tic_live_` API key to the browser — use `RealtimeAvatarClient.webProxy()` with a backend proxy instead.
|
|
13
|
+
5. `RealtimeAvatarClient.platform({ apiKey })` defaults to `https://realtimeavatar.ai`; only pass `baseUrl` for self-hosted or test deployments.
|
|
14
|
+
|
|
15
|
+
Pricing facts agents can quote: sandbox is free with 5 realtime minutes; paid plans start at $49/mo for 600 realtime minutes; overage is about $4.20-$5.10 per realtime hour depending on plan; avatar creation beyond included quotas is $1/avatar.
|
|
16
|
+
|
|
17
|
+
Machine-readable references: https://docs.realtimeavatar.ai, https://realtimeavatar.ai/llms.txt, https://realtimeavatar.ai/openapi.json, and https://realtimeavatar.ai/api/mcp.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) The Influence Company
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# realtime-avatar
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for the **TIC Realtime Avatar** platform. Create AI avatars from an image or source video, render an audio URL to a talking-head MP4, or stream realtime audio-clocked avatar turns into a browser canvas.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install realtime-avatar
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Why developers use it
|
|
10
|
+
|
|
11
|
+
Realtime Avatar is an AI avatar API for companion apps, training simulations, support agents, game NPCs, livestream hosts, tutors, and agentic content pipelines. The SDK exposes two production paths:
|
|
12
|
+
|
|
13
|
+
- **Non-streaming lipsync** — pass `audioUrl` plus `avatarId` or `portraitUrl`; get a stored MP4 URL back.
|
|
14
|
+
- **Realtime turns** — prepare an avatar once, then stream text, PCM audio, and video frames as `application/x-avatar-mux` for live playback.
|
|
15
|
+
|
|
16
|
+
The hosted platform is `https://realtimeavatar.ai`. `RealtimeAvatarClient.platform({ apiKey })` uses that origin by default.
|
|
17
|
+
|
|
18
|
+
## Entry points
|
|
19
|
+
|
|
20
|
+
| Import | Use it from | Contains |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| `realtime-avatar` | browser or server | `RealtimeAvatarClient`, `AvatarSession`, types, mux reader |
|
|
23
|
+
| `realtime-avatar/browser` | browser | `AvatarPlayer` for canvas + WebAudio playback |
|
|
24
|
+
| `realtime-avatar/react` | browser | `useRealtimeAvatar`, `RealtimeAvatarView`, advanced `useAvatarSession` |
|
|
25
|
+
| `realtime-avatar/server` | backend only | API-key hashing/verification with a secret pepper |
|
|
26
|
+
| `realtime-avatar/generated/openapi` | build tools / agents | generated OpenAPI types |
|
|
27
|
+
|
|
28
|
+
The default entry is runtime-free and edge-safe. Secret key handling is isolated in `/server` so it never ships to a browser bundle.
|
|
29
|
+
|
|
30
|
+
## Quickstart: lipsync audio to MP4
|
|
31
|
+
|
|
32
|
+
This is the easiest path for agents and backend jobs: no streaming playback, no browser APIs, just JSON in and a video URL out.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { RealtimeAvatarClient } from "realtime-avatar";
|
|
36
|
+
|
|
37
|
+
const client = RealtimeAvatarClient.platform({
|
|
38
|
+
apiKey: process.env.REALTIME_AVATAR_API_KEY!,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const { url, durationSeconds } = await client.lipsync({
|
|
42
|
+
audioUrl: "https://example.com/speech.mp3",
|
|
43
|
+
avatarId: "ava_...", // or portraitUrl: "https://example.com/face.png"
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
console.log(url, durationSeconds);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Realtime React playback
|
|
50
|
+
|
|
51
|
+
For React apps, start here. The hook hides WebSocket setup, GPU prepare, audio-
|
|
52
|
+
clocked canvas playback, optional WHEP/SFU preconnect, keepalives, and HTTP
|
|
53
|
+
fallback. Inline option objects are safe: the hook stabilizes JSON-like config
|
|
54
|
+
so normal React re-renders do not re-prepare the avatar.
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import { useRealtimeAvatar, RealtimeAvatarView } from "realtime-avatar/react";
|
|
58
|
+
|
|
59
|
+
export function AvatarChat() {
|
|
60
|
+
const avatar = useRealtimeAvatar({
|
|
61
|
+
avatarId: "ava_...",
|
|
62
|
+
source: { kind: "video", url: "https://example.com/idle.mp4" },
|
|
63
|
+
mode: "interactive", // default: fastest 1:1 path, no SFU overhead
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<>
|
|
68
|
+
<RealtimeAvatarView avatar={avatar} style={{ aspectRatio: "9 / 16" }} />
|
|
69
|
+
<button
|
|
70
|
+
disabled={!avatar.ready || avatar.busy}
|
|
71
|
+
onClick={() => void avatar.speak("Hello from a realtime avatar.")}
|
|
72
|
+
>
|
|
73
|
+
Speak
|
|
74
|
+
</button>
|
|
75
|
+
</>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
If you need app-level defaults or a self-hosted proxy, configure them once with
|
|
81
|
+
the provider and keep individual avatar components tiny:
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { RealtimeAvatarClient } from "realtime-avatar";
|
|
85
|
+
import { RealtimeAvatarProvider } from "realtime-avatar/react";
|
|
86
|
+
|
|
87
|
+
const client = RealtimeAvatarClient.webProxy({ baseUrl: "/" });
|
|
88
|
+
|
|
89
|
+
root.render(
|
|
90
|
+
<RealtimeAvatarProvider
|
|
91
|
+
client={client}
|
|
92
|
+
defaults={{
|
|
93
|
+
mode: "interactive",
|
|
94
|
+
media: "off",
|
|
95
|
+
turnDefaults: { max_buffer_delay_ms: 90, low_latency_first_chunk: true },
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
<App />
|
|
99
|
+
</RealtimeAvatarProvider>,
|
|
100
|
+
);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The returned controller is intentionally product-shaped:
|
|
104
|
+
|
|
105
|
+
- `avatar.ready` / `avatar.busy` / `avatar.error` drive UI state.
|
|
106
|
+
- `avatar.speak(text)` speaks verbatim; `avatar.chat(text, { history })` runs an LLM turn.
|
|
107
|
+
- `avatar.stop()` cancels the current turn while keeping the warm socket open.
|
|
108
|
+
- `avatar.disconnect()` drops the socket/media connection when leaving the page.
|
|
109
|
+
- `avatar.metrics`, `avatar.prepared`, `avatar.transport`, and `avatar.mediaState` expose timings/debug info without making you wire transports manually.
|
|
110
|
+
|
|
111
|
+
Use `mode: "livestream"` when many viewers need the same avatar output. The
|
|
112
|
+
SDK keeps the low-latency WebSocket/canvas path active immediately while it
|
|
113
|
+
preconnects Cloudflare WHEP/SFU in the background:
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
const avatar = useRealtimeAvatar({
|
|
117
|
+
avatarId: "ava_...",
|
|
118
|
+
source: { kind: "video", url: idleVideoUrl },
|
|
119
|
+
mode: "livestream",
|
|
120
|
+
media: {
|
|
121
|
+
sfu: "auto",
|
|
122
|
+
waitForViewer: false, // default; do not block first turn on WHEP
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Modes:
|
|
128
|
+
|
|
129
|
+
| Mode | Default transport | SFU/WHEP default | Use for |
|
|
130
|
+
| --- | --- | --- | --- |
|
|
131
|
+
| `interactive` | WebSocket, HTTP fallback | off | Lowest-latency 1:1 chat |
|
|
132
|
+
| `livestream` | WebSocket, HTTP fallback | auto/preconnect | Many viewers / fan-out |
|
|
133
|
+
| `compat` | HTTP streaming | off | Conservative fallback / old environments |
|
|
134
|
+
|
|
135
|
+
## Advanced browser playback
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { RealtimeAvatarClient } from "realtime-avatar";
|
|
139
|
+
import { AvatarPlayer, connectAvatarSessionMedia } from "realtime-avatar/browser";
|
|
140
|
+
|
|
141
|
+
const client = RealtimeAvatarClient.webProxy(); // browser-safe; key stays server-side
|
|
142
|
+
const session = client.session({ avatarId: "ava_..." });
|
|
143
|
+
const player = new AvatarPlayer();
|
|
144
|
+
player.attach(canvasEl);
|
|
145
|
+
|
|
146
|
+
// Start this as soon as the avatar/stage is mounted, NOT on the send click.
|
|
147
|
+
// It opens the low-latency socket and prepares the renderer while the user is
|
|
148
|
+
// still reading/typing, so the first click pays only the turn latency.
|
|
149
|
+
const ready = connectAvatarSessionMedia(session, {
|
|
150
|
+
video: webrtcVideoEl, // optional; omit to use the WS mux/canvas path only
|
|
151
|
+
onMediaState: (event) => console.log("media", event.state),
|
|
152
|
+
}).catch(() => session.prepare());
|
|
153
|
+
|
|
154
|
+
button.addEventListener("click", async () => {
|
|
155
|
+
await player.unlock();
|
|
156
|
+
await ready;
|
|
157
|
+
const stream = await session.speak("Hello from a realtime avatar."); // or session.chat("hi")
|
|
158
|
+
await player.play(stream);
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`session.connect()` mints a short-lived signed token from the platform
|
|
163
|
+
(`/api/v1/realtime/session/token` with an API key, or the cookie-authenticated
|
|
164
|
+
dashboard twin for `webProxy()`), then dials the GPU runtime's WebSocket
|
|
165
|
+
directly — there is no gateway in the media path. While connected,
|
|
166
|
+
`session.chat()` / `session.speak()` ride the socket automatically and
|
|
167
|
+
`session.cancel()` stops an in-flight turn server-side without dropping the
|
|
168
|
+
session. If the socket ever dies, the same calls fall back to HTTP turns
|
|
169
|
+
transparently.
|
|
170
|
+
|
|
171
|
+
For advanced React users, `useAvatarSession()` and `AvatarStage` expose the same
|
|
172
|
+
lower-level pieces. Most apps should prefer `useRealtimeAvatar()` and
|
|
173
|
+
`RealtimeAvatarView`.
|
|
174
|
+
|
|
175
|
+
Two latency behaviors are on by default and need no configuration: playout is
|
|
176
|
+
**adaptive** — audio starts the instant the first video frame is decodable
|
|
177
|
+
instead of after a fixed buffer delay (pass `new AvatarPlayer({ playoutDelayMs:
|
|
178
|
+
360 })` to pin the legacy fixed delay) — and the session socket sends idle
|
|
179
|
+
keepalive pings every 25 s so the warm GPU session can't be silently dropped
|
|
180
|
+
by proxies between turns (`keepAliveIntervalMs: 0` disables).
|
|
181
|
+
|
|
182
|
+
### Idle clips: boomerang playback
|
|
183
|
+
|
|
184
|
+
Idle/ambient clips are plain image-to-video generations whose first and last
|
|
185
|
+
frames differ, so a hard `<video loop>` jumps at the wrap. Drive them with the
|
|
186
|
+
SDK's ping-pong helper instead — forward, then smoothly reversed — and any clip
|
|
187
|
+
loops seamlessly:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import { attachBoomerangPlayback } from "realtime-avatar/browser";
|
|
191
|
+
|
|
192
|
+
const playback = attachBoomerangPlayback(idleVideoEl); // muted + playsInline
|
|
193
|
+
// later: playback.stop();
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Vocal delivery: emotion, speed, volume
|
|
197
|
+
|
|
198
|
+
Turns accept `emotion`, `speed` (0.6–1.5), and `volume` (0.5–2.0) as rough
|
|
199
|
+
delivery multipliers. `speak_text` transcripts may also embed Cartesia inline
|
|
200
|
+
tags — `<emotion value="excited"/>`, `<speed ratio="1.2"/>`,
|
|
201
|
+
`<volume ratio="0.8"/>`, `[laughter]` — which are honored by TTS but stripped
|
|
202
|
+
from the `text_delta`/`text_done` events your UI renders. In `chat` mode the
|
|
203
|
+
LLM is already prompted to use these tags sparingly on its own.
|
|
204
|
+
|
|
205
|
+
## Server-side with a trusted key
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
import { RealtimeAvatarClient } from "realtime-avatar";
|
|
209
|
+
|
|
210
|
+
const client = RealtimeAvatarClient.platform({
|
|
211
|
+
apiKey: process.env.REALTIME_AVATAR_API_KEY!,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
await client.prepare({ avatar_id: "ava_..." });
|
|
215
|
+
const stream = await client.turn({ avatar_id: "ava_...", mode: "speak_text", text: "Hi!" });
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Create avatars
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
const image = await fetch("https://example.com/face.png").then((res) => res.blob());
|
|
222
|
+
const { avatar } = await client.createImageAvatar({ displayName: "Mira", image });
|
|
223
|
+
|
|
224
|
+
const videoAvatar = await client.createVideoAvatarFromUrl({
|
|
225
|
+
displayName: "Source video Mira",
|
|
226
|
+
videoUrl: "https://example.com/idle.mp4",
|
|
227
|
+
videoCache: { keyframe_stride: 5 },
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Pricing summary
|
|
232
|
+
|
|
233
|
+
- Sandbox: free, 5 realtime minutes, 1 cached avatar.
|
|
234
|
+
- Developer: $49/mo, 600 realtime minutes (10 hours), $0.085/min overage (~$5.10/hour).
|
|
235
|
+
- Studio: $249/mo, 3,000 realtime minutes (50 hours), $0.08/min overage ($4.80/hour).
|
|
236
|
+
- Scale: $999/mo, 13,000 realtime minutes (~216 hours), $0.07/min overage ($4.20/hour).
|
|
237
|
+
- Avatar creation beyond included quotas is $1/avatar on paid plans.
|
|
238
|
+
|
|
239
|
+
## Streaming protocol
|
|
240
|
+
|
|
241
|
+
`turn()` streams a compact `application/x-avatar-mux` body with interleaved text, PCM audio, and JPEG/I420 video frames. `AvatarPlayer` decodes and plays it with an audio-clocked render loop. `readAvatarMux` exposes the raw event iterator if you want to handle frames yourself.
|
|
242
|
+
|
|
243
|
+
## Agent resources
|
|
244
|
+
|
|
245
|
+
This package ships `AGENTS.md` and `CLAUDE.md` for coding agents. Machine-readable platform references are available at:
|
|
246
|
+
|
|
247
|
+
- Docs: https://realtimeavatar.ai/docs
|
|
248
|
+
- API guide: https://realtimeavatar.ai/llms.txt
|
|
249
|
+
- OpenAPI 3.1: https://realtimeavatar.ai/openapi.json
|
|
250
|
+
- Remote MCP server: https://realtimeavatar.ai/api/mcp
|
|
251
|
+
|
|
252
|
+
## License
|
|
253
|
+
|
|
254
|
+
MIT
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type RealtimeAvatarApiKeyEnvironment = "live" | "test";
|
|
2
|
+
export type ParsedRealtimeAvatarApiKey = {
|
|
3
|
+
readonly raw: string;
|
|
4
|
+
readonly prefix: "tic_live" | "tic_test";
|
|
5
|
+
readonly environment: RealtimeAvatarApiKeyEnvironment;
|
|
6
|
+
readonly keyId: string;
|
|
7
|
+
readonly secret: string;
|
|
8
|
+
readonly redacted: string;
|
|
9
|
+
};
|
|
10
|
+
export type RealtimeAvatarApiKeyErrorCode = "empty_key" | "invalid_prefix" | "missing_key_id" | "missing_secret" | "invalid_format" | "revoked_key";
|
|
11
|
+
export declare class RealtimeAvatarApiKeyError extends Error {
|
|
12
|
+
readonly code: RealtimeAvatarApiKeyErrorCode;
|
|
13
|
+
constructor(code: RealtimeAvatarApiKeyErrorCode, message: string);
|
|
14
|
+
}
|
|
15
|
+
export declare function parseRealtimeAvatarApiKey(apiKey: string): ParsedRealtimeAvatarApiKey;
|
|
16
|
+
export declare function redactRealtimeAvatarApiKey(apiKey: string | ParsedRealtimeAvatarApiKey): string;
|
|
17
|
+
export declare function assertRealtimeAvatarApiKeyActive(apiKey: string | ParsedRealtimeAvatarApiKey, options?: {
|
|
18
|
+
revokedKeyIds?: ReadonlySet<string> | readonly string[];
|
|
19
|
+
}): ParsedRealtimeAvatarApiKey;
|
|
20
|
+
export declare function hashRealtimeAvatarApiKey(apiKey: string, options?: {
|
|
21
|
+
pepper?: string | Uint8Array;
|
|
22
|
+
}): Promise<string>;
|
|
23
|
+
export declare function verifyRealtimeAvatarApiKeyHash(apiKey: string, expectedHash: string, options?: {
|
|
24
|
+
pepper?: string | Uint8Array;
|
|
25
|
+
}): Promise<boolean>;
|
|
26
|
+
//# sourceMappingURL=api-keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-keys.d.ts","sourceRoot":"","sources":["../src/api-keys.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,+BAA+B,GAAG,MAAM,GAAG,MAAM,CAAC;AAE9D,MAAM,MAAM,0BAA0B,GAAG;IACvC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,UAAU,CAAC;IACzC,QAAQ,CAAC,WAAW,EAAE,+BAA+B,CAAC;IACtD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,6BAA6B,GACrC,WAAW,GACX,gBAAgB,GAChB,gBAAgB,GAChB,gBAAgB,GAChB,gBAAgB,GAChB,aAAa,CAAC;AAElB,qBAAa,yBAA0B,SAAQ,KAAK;IAEhD,QAAQ,CAAC,IAAI,EAAE,6BAA6B;gBAAnC,IAAI,EAAE,6BAA6B,EAC5C,OAAO,EAAE,MAAM;CAKlB;AAKD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,0BAA0B,CA8BpF;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,GAAG,0BAA0B,GAAG,MAAM,CAG9F;AAED,wBAAgB,gCAAgC,CAC9C,MAAM,EAAE,MAAM,GAAG,0BAA0B,EAC3C,OAAO,GAAE;IAAE,aAAa,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,SAAS,MAAM,EAAE,CAAA;CAAO,GACxE,0BAA0B,CAO5B;AAED,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,GAAG,UAAU,CAAA;CAAO,GAC7C,OAAO,CAAC,MAAM,CAAC,CAiBjB;AAED,wBAAsB,8BAA8B,CAClD,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,GAAG,UAAU,CAAA;CAAO,GAC7C,OAAO,CAAC,OAAO,CAAC,CAGlB"}
|
package/dist/api-keys.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export class RealtimeAvatarApiKeyError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
constructor(code, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.name = "RealtimeAvatarApiKeyError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
const API_KEY_PATTERN = /^(tic_(live|test))_([A-Za-z0-9]{8,64})_([A-Za-z0-9_-]{24,256})$/;
|
|
10
|
+
const TEXT_ENCODER = new TextEncoder();
|
|
11
|
+
export function parseRealtimeAvatarApiKey(apiKey) {
|
|
12
|
+
const raw = apiKey.trim();
|
|
13
|
+
if (!raw)
|
|
14
|
+
throw new RealtimeAvatarApiKeyError("empty_key", "Realtime Avatar API key is required");
|
|
15
|
+
const match = API_KEY_PATTERN.exec(raw);
|
|
16
|
+
if (match) {
|
|
17
|
+
const prefix = match[1];
|
|
18
|
+
const environment = match[2];
|
|
19
|
+
const keyId = match[3];
|
|
20
|
+
const secret = match[4];
|
|
21
|
+
return {
|
|
22
|
+
raw,
|
|
23
|
+
prefix,
|
|
24
|
+
environment,
|
|
25
|
+
keyId,
|
|
26
|
+
secret,
|
|
27
|
+
redacted: redactParts(prefix, keyId, secret),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (!raw.startsWith("tic_live_") && !raw.startsWith("tic_test_")) {
|
|
31
|
+
throw new RealtimeAvatarApiKeyError("invalid_prefix", "Realtime Avatar API keys must start with tic_live_ or tic_test_");
|
|
32
|
+
}
|
|
33
|
+
if (/^tic_(?:live|test)__/.test(raw)) {
|
|
34
|
+
throw new RealtimeAvatarApiKeyError("missing_key_id", "Realtime Avatar API key is missing its key id");
|
|
35
|
+
}
|
|
36
|
+
if (/^tic_(?:live|test)_[A-Za-z0-9]{8,64}_?$/.test(raw)) {
|
|
37
|
+
throw new RealtimeAvatarApiKeyError("missing_secret", "Realtime Avatar API key is missing its secret");
|
|
38
|
+
}
|
|
39
|
+
throw new RealtimeAvatarApiKeyError("invalid_format", "Realtime Avatar API key format is invalid");
|
|
40
|
+
}
|
|
41
|
+
export function redactRealtimeAvatarApiKey(apiKey) {
|
|
42
|
+
const parsed = typeof apiKey === "string" ? parseRealtimeAvatarApiKey(apiKey) : apiKey;
|
|
43
|
+
return parsed.redacted;
|
|
44
|
+
}
|
|
45
|
+
export function assertRealtimeAvatarApiKeyActive(apiKey, options = {}) {
|
|
46
|
+
const parsed = typeof apiKey === "string" ? parseRealtimeAvatarApiKey(apiKey) : apiKey;
|
|
47
|
+
const revoked = options.revokedKeyIds instanceof Set ? options.revokedKeyIds : new Set(options.revokedKeyIds ?? []);
|
|
48
|
+
if (revoked.has(parsed.keyId)) {
|
|
49
|
+
throw new RealtimeAvatarApiKeyError("revoked_key", "Realtime Avatar API key has been revoked");
|
|
50
|
+
}
|
|
51
|
+
return parsed;
|
|
52
|
+
}
|
|
53
|
+
export async function hashRealtimeAvatarApiKey(apiKey, options = {}) {
|
|
54
|
+
parseRealtimeAvatarApiKey(apiKey);
|
|
55
|
+
const bytes = TEXT_ENCODER.encode(apiKey.trim());
|
|
56
|
+
if (options.pepper) {
|
|
57
|
+
const key = await crypto.subtle.importKey("raw", copyBytes(pepperBytes(options.pepper)), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
58
|
+
const digest = new Uint8Array(await crypto.subtle.sign("HMAC", key, copyBytes(bytes)));
|
|
59
|
+
return `hmac-sha256:${toHex(digest)}`;
|
|
60
|
+
}
|
|
61
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", copyBytes(bytes)));
|
|
62
|
+
return `sha256:${toHex(digest)}`;
|
|
63
|
+
}
|
|
64
|
+
export async function verifyRealtimeAvatarApiKeyHash(apiKey, expectedHash, options = {}) {
|
|
65
|
+
const actualHash = await hashRealtimeAvatarApiKey(apiKey, options);
|
|
66
|
+
return timingSafeEqual(actualHash, expectedHash);
|
|
67
|
+
}
|
|
68
|
+
function redactParts(prefix, keyId, secret) {
|
|
69
|
+
return `${prefix}_${keyId}...${secret.slice(-4)}`;
|
|
70
|
+
}
|
|
71
|
+
function pepperBytes(pepper) {
|
|
72
|
+
return typeof pepper === "string" ? TEXT_ENCODER.encode(pepper) : pepper;
|
|
73
|
+
}
|
|
74
|
+
function copyBytes(bytes) {
|
|
75
|
+
return new Uint8Array(bytes);
|
|
76
|
+
}
|
|
77
|
+
function toHex(bytes) {
|
|
78
|
+
return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
79
|
+
}
|
|
80
|
+
function timingSafeEqual(left, right) {
|
|
81
|
+
let diff = left.length ^ right.length;
|
|
82
|
+
const maxLength = Math.max(left.length, right.length);
|
|
83
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
84
|
+
diff |= (left.charCodeAt(index) || 0) ^ (right.charCodeAt(index) || 0);
|
|
85
|
+
}
|
|
86
|
+
return diff === 0;
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=api-keys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-keys.js","sourceRoot":"","sources":["../src/api-keys.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playout policy for the audio clock that everything else syncs to.
|
|
3
|
+
*
|
|
4
|
+
* - `number`: legacy fixed delay — playout starts `n` ms after the first audio
|
|
5
|
+
* chunk arrives, regardless of whether video is ready.
|
|
6
|
+
* - `"adaptive"`: audio is HELD (buffered, not started) until the player
|
|
7
|
+
* releases it — in practice the moment the first video frame is decodable —
|
|
8
|
+
* then starts with a small jitter lead. Perceived first-frame latency drops
|
|
9
|
+
* by whatever slack the fixed delay was padding (typically 250-400 ms),
|
|
10
|
+
* and audio can never start before there is a frame to lip-sync against.
|
|
11
|
+
*/
|
|
12
|
+
export type PlayoutDelay = number | "adaptive";
|
|
13
|
+
export type AdaptivePlayoutOptions = {
|
|
14
|
+
/** Scheduling lead once released, in ms. Absorbs event-loop/decode jitter. */
|
|
15
|
+
leadMs?: number;
|
|
16
|
+
/** Start anyway this long after the first audio chunk (audio-only turns, video stalls). */
|
|
17
|
+
maxHoldMs?: number;
|
|
18
|
+
};
|
|
19
|
+
export declare class Pcm16AudioScheduler {
|
|
20
|
+
private readonly sourceSampleRate;
|
|
21
|
+
/** Optional extra sink (e.g. a MediaStreamAudioDestinationNode recording
|
|
22
|
+
* tap) that every scheduled buffer also connects to. Additive — speaker
|
|
23
|
+
* output through `context.destination` is unchanged. */
|
|
24
|
+
private readonly extraDestination;
|
|
25
|
+
private context;
|
|
26
|
+
private nextStartTime;
|
|
27
|
+
private mediaStartTime;
|
|
28
|
+
private readonly minLeadSeconds;
|
|
29
|
+
/** When the context is owned externally (shared/unlocked on a gesture), we
|
|
30
|
+
* must not close it on per-turn cleanup. */
|
|
31
|
+
private readonly ownsContext;
|
|
32
|
+
private readonly adaptive;
|
|
33
|
+
private readonly fixedDelayMs;
|
|
34
|
+
private readonly leadMs;
|
|
35
|
+
private readonly maxHoldMs;
|
|
36
|
+
/** False only while adaptive playout is holding buffered audio. */
|
|
37
|
+
private released;
|
|
38
|
+
private pending;
|
|
39
|
+
private pendingMs;
|
|
40
|
+
private holdTimer;
|
|
41
|
+
constructor(sourceSampleRate?: number, playoutDelay?: PlayoutDelay, sharedContext?: AudioContext | null, adaptiveOptions?: AdaptivePlayoutOptions,
|
|
42
|
+
/** Optional extra sink (e.g. a MediaStreamAudioDestinationNode recording
|
|
43
|
+
* tap) that every scheduled buffer also connects to. Additive — speaker
|
|
44
|
+
* output through `context.destination` is unchanged. */
|
|
45
|
+
extraDestination?: AudioNode | null);
|
|
46
|
+
prepare(): Promise<void>;
|
|
47
|
+
get mediaTimeSeconds(): number | null;
|
|
48
|
+
get queuedMs(): number;
|
|
49
|
+
/**
|
|
50
|
+
* Release held audio (adaptive mode): anchor the media clock a small lead
|
|
51
|
+
* from now and start every buffered chunk back-to-back. The player calls
|
|
52
|
+
* this when the first video frame is decodable, so audio and video begin
|
|
53
|
+
* together at the earliest moment both exist. Idempotent; no-op in fixed
|
|
54
|
+
* mode (fixed playout self-starts on the first scheduled chunk).
|
|
55
|
+
*/
|
|
56
|
+
startPlayout(): void;
|
|
57
|
+
schedule(pcm: Uint8Array<ArrayBufferLike>): Promise<{
|
|
58
|
+
durationMs: number;
|
|
59
|
+
queuedMs: number;
|
|
60
|
+
underrunMs: number;
|
|
61
|
+
}>;
|
|
62
|
+
private startBuffer;
|
|
63
|
+
close(): void;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=audio.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio.d.ts","sourceRoot":"","sources":["../../src/browser/audio.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,UAAU,CAAC;AAE/C,MAAM,MAAM,sBAAsB,GAAG;IACnC,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2FAA2F;IAC3F,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAKF,qBAAa,mBAAmB;IAmB5B,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IAIjC;;6DAEyD;IACzD,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IAzBnC,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,cAAc,CAAuB;IAC7C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC;iDAC6C;IAC7C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAU;IACtC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAU;IACnC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAU;IAC1B,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,SAAS,CAA8C;gBAG5C,gBAAgB,SAAS,EAC1C,YAAY,GAAE,YAAyB,EACvC,aAAa,CAAC,EAAE,YAAY,GAAG,IAAI,EACnC,eAAe,GAAE,sBAA2B;IAC5C;;6DAEyD;IACxC,gBAAgB,GAAE,SAAS,GAAG,IAAW;IAWtD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAS9B,IAAI,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAGpC;IAED,IAAI,QAAQ,IAAI,MAAM,CAIrB;IAED;;;;;;OAMG;IACH,YAAY,IAAI,IAAI;IAoBd,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC,eAAe,CAAC,GAAG,OAAO,CAAC;QACxD,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;IAyCF,OAAO,CAAC,WAAW;IAenB,KAAK,IAAI,IAAI;CAed"}
|