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
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
const ADAPTIVE_LEAD_MS = 75;
|
|
2
|
+
const ADAPTIVE_MAX_HOLD_MS = 400;
|
|
3
|
+
export class Pcm16AudioScheduler {
|
|
4
|
+
sourceSampleRate;
|
|
5
|
+
extraDestination;
|
|
6
|
+
context = null;
|
|
7
|
+
nextStartTime = 0;
|
|
8
|
+
mediaStartTime = null;
|
|
9
|
+
minLeadSeconds = 0.045;
|
|
10
|
+
/** When the context is owned externally (shared/unlocked on a gesture), we
|
|
11
|
+
* must not close it on per-turn cleanup. */
|
|
12
|
+
ownsContext;
|
|
13
|
+
adaptive;
|
|
14
|
+
fixedDelayMs;
|
|
15
|
+
leadMs;
|
|
16
|
+
maxHoldMs;
|
|
17
|
+
/** False only while adaptive playout is holding buffered audio. */
|
|
18
|
+
released;
|
|
19
|
+
pending = [];
|
|
20
|
+
pendingMs = 0;
|
|
21
|
+
holdTimer = null;
|
|
22
|
+
constructor(sourceSampleRate = 16_000, playoutDelay = "adaptive", sharedContext, adaptiveOptions = {},
|
|
23
|
+
/** Optional extra sink (e.g. a MediaStreamAudioDestinationNode recording
|
|
24
|
+
* tap) that every scheduled buffer also connects to. Additive — speaker
|
|
25
|
+
* output through `context.destination` is unchanged. */
|
|
26
|
+
extraDestination = null) {
|
|
27
|
+
this.sourceSampleRate = sourceSampleRate;
|
|
28
|
+
this.extraDestination = extraDestination;
|
|
29
|
+
this.context = sharedContext ?? null;
|
|
30
|
+
this.ownsContext = !sharedContext;
|
|
31
|
+
this.adaptive = playoutDelay === "adaptive";
|
|
32
|
+
this.fixedDelayMs = typeof playoutDelay === "number" ? playoutDelay : 0;
|
|
33
|
+
this.leadMs = adaptiveOptions.leadMs ?? ADAPTIVE_LEAD_MS;
|
|
34
|
+
this.maxHoldMs = adaptiveOptions.maxHoldMs ?? ADAPTIVE_MAX_HOLD_MS;
|
|
35
|
+
this.released = !this.adaptive;
|
|
36
|
+
}
|
|
37
|
+
async prepare() {
|
|
38
|
+
const AudioContextCtor = window.AudioContext ||
|
|
39
|
+
window.webkitAudioContext;
|
|
40
|
+
if (!AudioContextCtor)
|
|
41
|
+
throw new Error("WebAudio is not available in this browser");
|
|
42
|
+
this.context ??= new AudioContextCtor({ latencyHint: "interactive" });
|
|
43
|
+
if (this.context.state !== "running")
|
|
44
|
+
await this.context.resume();
|
|
45
|
+
}
|
|
46
|
+
get mediaTimeSeconds() {
|
|
47
|
+
if (!this.context || this.mediaStartTime === null)
|
|
48
|
+
return null;
|
|
49
|
+
return Math.max(0, this.context.currentTime - this.mediaStartTime);
|
|
50
|
+
}
|
|
51
|
+
get queuedMs() {
|
|
52
|
+
if (!this.released)
|
|
53
|
+
return this.pendingMs;
|
|
54
|
+
if (!this.context)
|
|
55
|
+
return 0;
|
|
56
|
+
return Math.max(0, (this.nextStartTime - this.context.currentTime) * 1000);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Release held audio (adaptive mode): anchor the media clock a small lead
|
|
60
|
+
* from now and start every buffered chunk back-to-back. The player calls
|
|
61
|
+
* this when the first video frame is decodable, so audio and video begin
|
|
62
|
+
* together at the earliest moment both exist. Idempotent; no-op in fixed
|
|
63
|
+
* mode (fixed playout self-starts on the first scheduled chunk).
|
|
64
|
+
*/
|
|
65
|
+
startPlayout() {
|
|
66
|
+
if (this.released)
|
|
67
|
+
return;
|
|
68
|
+
this.released = true;
|
|
69
|
+
if (this.holdTimer !== null) {
|
|
70
|
+
clearTimeout(this.holdTimer);
|
|
71
|
+
this.holdTimer = null;
|
|
72
|
+
}
|
|
73
|
+
const context = this.context;
|
|
74
|
+
if (!context || this.pending.length === 0) {
|
|
75
|
+
this.pending = [];
|
|
76
|
+
this.pendingMs = 0;
|
|
77
|
+
return; // nothing buffered yet: the next schedule() anchors the clock
|
|
78
|
+
}
|
|
79
|
+
this.mediaStartTime = context.currentTime + this.leadMs / 1000;
|
|
80
|
+
this.nextStartTime = this.mediaStartTime;
|
|
81
|
+
for (const buffer of this.pending)
|
|
82
|
+
this.startBuffer(context, buffer);
|
|
83
|
+
this.pending = [];
|
|
84
|
+
this.pendingMs = 0;
|
|
85
|
+
}
|
|
86
|
+
async schedule(pcm) {
|
|
87
|
+
await this.prepare();
|
|
88
|
+
const context = this.context;
|
|
89
|
+
if (!context)
|
|
90
|
+
throw new Error("AudioContext did not initialize");
|
|
91
|
+
const samples = Math.floor(pcm.byteLength / 2);
|
|
92
|
+
const audioBuffer = context.createBuffer(1, samples, this.sourceSampleRate);
|
|
93
|
+
const channel = audioBuffer.getChannelData(0);
|
|
94
|
+
const view = new DataView(pcm.buffer, pcm.byteOffset, pcm.byteLength);
|
|
95
|
+
for (let i = 0; i < samples; i += 1) {
|
|
96
|
+
channel[i] = Math.max(-1, Math.min(1, view.getInt16(i * 2, true) / 32768));
|
|
97
|
+
}
|
|
98
|
+
const durationMs = audioBuffer.duration * 1000;
|
|
99
|
+
if (!this.released) {
|
|
100
|
+
this.pending.push(audioBuffer);
|
|
101
|
+
this.pendingMs += durationMs;
|
|
102
|
+
// Video may never come (audio-only turn, decoder fault): don't hold forever.
|
|
103
|
+
this.holdTimer ??= setTimeout(() => this.startPlayout(), this.maxHoldMs);
|
|
104
|
+
return { durationMs, queuedMs: this.queuedMs, underrunMs: 0 };
|
|
105
|
+
}
|
|
106
|
+
if (this.mediaStartTime === null) {
|
|
107
|
+
const delayMs = this.adaptive ? this.leadMs : this.fixedDelayMs;
|
|
108
|
+
this.mediaStartTime = context.currentTime + delayMs / 1000;
|
|
109
|
+
this.nextStartTime = this.mediaStartTime;
|
|
110
|
+
}
|
|
111
|
+
let underrunMs = 0;
|
|
112
|
+
const minimumStartTime = context.currentTime + this.minLeadSeconds;
|
|
113
|
+
if (this.nextStartTime < minimumStartTime) {
|
|
114
|
+
const correctionSeconds = minimumStartTime - this.nextStartTime;
|
|
115
|
+
this.nextStartTime = minimumStartTime;
|
|
116
|
+
this.mediaStartTime += correctionSeconds;
|
|
117
|
+
underrunMs = correctionSeconds * 1000;
|
|
118
|
+
}
|
|
119
|
+
this.startBuffer(context, audioBuffer);
|
|
120
|
+
return { durationMs, queuedMs: this.queuedMs, underrunMs };
|
|
121
|
+
}
|
|
122
|
+
startBuffer(context, audioBuffer) {
|
|
123
|
+
const source = context.createBufferSource();
|
|
124
|
+
source.buffer = audioBuffer;
|
|
125
|
+
source.connect(context.destination);
|
|
126
|
+
if (this.extraDestination) {
|
|
127
|
+
try {
|
|
128
|
+
source.connect(this.extraDestination);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// A tap from a different/closed context must never break playback.
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
source.start(this.nextStartTime);
|
|
135
|
+
this.nextStartTime += audioBuffer.duration;
|
|
136
|
+
}
|
|
137
|
+
close() {
|
|
138
|
+
if (this.holdTimer !== null) {
|
|
139
|
+
clearTimeout(this.holdTimer);
|
|
140
|
+
this.holdTimer = null;
|
|
141
|
+
}
|
|
142
|
+
this.pending = [];
|
|
143
|
+
this.pendingMs = 0;
|
|
144
|
+
const context = this.context;
|
|
145
|
+
this.context = null;
|
|
146
|
+
this.mediaStartTime = null;
|
|
147
|
+
this.nextStartTime = 0;
|
|
148
|
+
// Only tear down a context this scheduler created. A shared/unlocked context
|
|
149
|
+
// is owned by the player and reused across turns.
|
|
150
|
+
if (this.ownsContext && context && context.state !== "closed")
|
|
151
|
+
void context.close();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=audio.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio.js","sourceRoot":"","sources":["../../src/browser/audio.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ping-pong (boomerang) playback for idle/ambient clips.
|
|
3
|
+
*
|
|
4
|
+
* Idle videos are plain image-to-video generations (start frame != end
|
|
5
|
+
* frame), so a hard `loop` visibly jumps at the wrap. Playing forward, then
|
|
6
|
+
* scrubbing back to the start, makes ANY clip loop seamlessly — no seamless
|
|
7
|
+
* A==B generation required. Reverse is done by stepping `currentTime` on a
|
|
8
|
+
* requestAnimationFrame clock because `playbackRate < 0` is unsupported in
|
|
9
|
+
* most browsers.
|
|
10
|
+
*/
|
|
11
|
+
export type BoomerangPlaybackOptions = {
|
|
12
|
+
/** Reverse-phase speed relative to real time. Default 1 (same pace as forward). */
|
|
13
|
+
reverseRate?: number;
|
|
14
|
+
/** Seconds from each end at which the direction flips. Default 0.05. */
|
|
15
|
+
edgeEpsilonSeconds?: number;
|
|
16
|
+
};
|
|
17
|
+
export type BoomerangPlayback = {
|
|
18
|
+
/** Stop driving the element (leaves the video itself untouched). */
|
|
19
|
+
stop(): void;
|
|
20
|
+
/** Current loop state, e.g. to hand playback position to a live renderer. */
|
|
21
|
+
position(): {
|
|
22
|
+
timeSeconds: number;
|
|
23
|
+
direction: "forward" | "reverse";
|
|
24
|
+
};
|
|
25
|
+
/** Jump the loop to a position+direction (seamless handback from a live render). */
|
|
26
|
+
alignTo(timeSeconds: number, direction: "forward" | "reverse"): void;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Drive a `<video>` element in a forward-then-reverse loop. The element should
|
|
30
|
+
* be muted + playsInline (autoplay policies). Idempotent per call; returns a
|
|
31
|
+
* handle whose `stop()` cancels the drive loop. Safe with elements whose
|
|
32
|
+
* metadata has not loaded yet.
|
|
33
|
+
*
|
|
34
|
+
* const playback = attachBoomerangPlayback(videoEl);
|
|
35
|
+
* // later: playback.stop();
|
|
36
|
+
*/
|
|
37
|
+
export declare function attachBoomerangPlayback(video: HTMLVideoElement, options?: BoomerangPlaybackOptions): BoomerangPlayback;
|
|
38
|
+
//# sourceMappingURL=boomerang.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"boomerang.d.ts","sourceRoot":"","sources":["../../src/browser/boomerang.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,MAAM,wBAAwB,GAAG;IACrC,mFAAmF;IACnF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,oEAAoE;IACpE,IAAI,IAAI,IAAI,CAAC;IACb,6EAA6E;IAC7E,QAAQ,IAAI;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,SAAS,GAAG,SAAS,CAAA;KAAE,CAAC;IACtE,oFAAoF;IACpF,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,SAAS,GAAG,IAAI,CAAC;CACtE,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,gBAAgB,EACvB,OAAO,GAAE,wBAA6B,GACrC,iBAAiB,CA4DnB"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ping-pong (boomerang) playback for idle/ambient clips.
|
|
3
|
+
*
|
|
4
|
+
* Idle videos are plain image-to-video generations (start frame != end
|
|
5
|
+
* frame), so a hard `loop` visibly jumps at the wrap. Playing forward, then
|
|
6
|
+
* scrubbing back to the start, makes ANY clip loop seamlessly — no seamless
|
|
7
|
+
* A==B generation required. Reverse is done by stepping `currentTime` on a
|
|
8
|
+
* requestAnimationFrame clock because `playbackRate < 0` is unsupported in
|
|
9
|
+
* most browsers.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Drive a `<video>` element in a forward-then-reverse loop. The element should
|
|
13
|
+
* be muted + playsInline (autoplay policies). Idempotent per call; returns a
|
|
14
|
+
* handle whose `stop()` cancels the drive loop. Safe with elements whose
|
|
15
|
+
* metadata has not loaded yet.
|
|
16
|
+
*
|
|
17
|
+
* const playback = attachBoomerangPlayback(videoEl);
|
|
18
|
+
* // later: playback.stop();
|
|
19
|
+
*/
|
|
20
|
+
export function attachBoomerangPlayback(video, options = {}) {
|
|
21
|
+
const reverseRate = options.reverseRate ?? 1;
|
|
22
|
+
const edge = options.edgeEpsilonSeconds ?? 0.05;
|
|
23
|
+
let raf = null;
|
|
24
|
+
let direction = 1;
|
|
25
|
+
let last = now();
|
|
26
|
+
const step = (nowMs) => {
|
|
27
|
+
raf = requestAnimationFrame(step);
|
|
28
|
+
const duration = video.duration;
|
|
29
|
+
if (!Number.isFinite(duration) || duration <= 0) {
|
|
30
|
+
last = nowMs;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (direction === 1) {
|
|
34
|
+
// Forward playback is native; just watch for the end to flip direction.
|
|
35
|
+
if (video.paused)
|
|
36
|
+
void video.play().catch(() => { });
|
|
37
|
+
if (video.currentTime >= duration - edge) {
|
|
38
|
+
direction = -1;
|
|
39
|
+
video.pause();
|
|
40
|
+
last = nowMs;
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// Reverse: manually rewind currentTime using the wall-clock delta. The
|
|
45
|
+
// delta is clamped so a background-tab pause (rAF suspension) resumes
|
|
46
|
+
// smoothly instead of teleporting.
|
|
47
|
+
const dt = Math.min(0.1, (nowMs - last) / 1000) * reverseRate;
|
|
48
|
+
last = nowMs;
|
|
49
|
+
const next = video.currentTime - dt;
|
|
50
|
+
if (next <= edge) {
|
|
51
|
+
video.currentTime = 0;
|
|
52
|
+
direction = 1;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
video.currentTime = next;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
raf = requestAnimationFrame(step);
|
|
59
|
+
return {
|
|
60
|
+
stop() {
|
|
61
|
+
if (raf !== null) {
|
|
62
|
+
cancelAnimationFrame(raf);
|
|
63
|
+
raf = null;
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
position() {
|
|
67
|
+
return { timeSeconds: video.currentTime, direction: direction === 1 ? "forward" : "reverse" };
|
|
68
|
+
},
|
|
69
|
+
alignTo(timeSeconds, nextDirection) {
|
|
70
|
+
const duration = video.duration;
|
|
71
|
+
const clamped = Number.isFinite(duration) && duration > 0
|
|
72
|
+
? Math.min(Math.max(timeSeconds, 0), duration - edge)
|
|
73
|
+
: Math.max(timeSeconds, 0);
|
|
74
|
+
video.currentTime = clamped;
|
|
75
|
+
direction = nextDirection === "reverse" ? -1 : 1;
|
|
76
|
+
last = now();
|
|
77
|
+
if (direction === -1)
|
|
78
|
+
video.pause();
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function now() {
|
|
83
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=boomerang.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"boomerang.js","sourceRoot":"","sources":["../../src/browser/boomerang.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { Pcm16AudioScheduler } from "./audio";
|
|
2
|
+
export { attachBoomerangPlayback, type BoomerangPlayback, type BoomerangPlaybackOptions, } from "./boomerang";
|
|
3
|
+
export { I420CanvasRenderer, drawI420Frame } from "./yuv-canvas";
|
|
4
|
+
export { AvatarPlayer, type AvatarPlayerState, type AvatarPlayerMetrics, type AvatarPlayerOptions, type AvatarPlayHandlers, type AvatarPlaySummary, type AvatarSourceVideoState, } from "./player";
|
|
5
|
+
export { CanvasView, type AvatarView, type AvatarViewMetrics } from "./view";
|
|
6
|
+
export { attachCloudflareWhepPlayback, type CloudflareWhepPlayback, type CloudflareWhepPlaybackOptions, } from "./webrtc";
|
|
7
|
+
export { connectAvatarSessionMedia, type AvatarSessionMediaConnection, type AvatarSessionMediaState, type AvatarSessionMediaStateEvent, type ConnectAvatarSessionMediaOptions, } from "./media-session";
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/browser/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,EACL,uBAAuB,EACvB,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,GAC9B,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EACL,YAAY,EACZ,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,GAC5B,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAC7E,OAAO,EACL,4BAA4B,EAC5B,KAAK,sBAAsB,EAC3B,KAAK,6BAA6B,GACnC,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,yBAAyB,EACzB,KAAK,4BAA4B,EACjC,KAAK,uBAAuB,EAC5B,KAAK,4BAA4B,EACjC,KAAK,gCAAgC,GACtC,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { Pcm16AudioScheduler } from "./audio";
|
|
2
|
+
export { attachBoomerangPlayback, } from "./boomerang";
|
|
3
|
+
export { I420CanvasRenderer, drawI420Frame } from "./yuv-canvas";
|
|
4
|
+
export { AvatarPlayer, } from "./player";
|
|
5
|
+
export { CanvasView } from "./view";
|
|
6
|
+
export { attachCloudflareWhepPlayback, } from "./webrtc";
|
|
7
|
+
export { connectAvatarSessionMedia, } from "./media-session";
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/browser/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,EACL,uBAAuB,GAGxB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EACL,YAAY,GAOb,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,UAAU,EAA2C,MAAM,QAAQ,CAAC;AAC7E,OAAO,EACL,4BAA4B,GAG7B,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,yBAAyB,GAK1B,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type RealtimeMediaPlan } from "../media";
|
|
2
|
+
import type { AvatarSession, AvatarSessionConnectResult } from "../session";
|
|
3
|
+
import { type CloudflareWhepPlayback, type CloudflareWhepPlaybackOptions } from "./webrtc";
|
|
4
|
+
export type AvatarSessionMediaState = "disabled" | "unsupported" | "connecting" | "connected" | "playing" | "error";
|
|
5
|
+
export type AvatarSessionMediaStateEvent = {
|
|
6
|
+
state: AvatarSessionMediaState;
|
|
7
|
+
media: RealtimeMediaPlan | null;
|
|
8
|
+
playback?: CloudflareWhepPlayback;
|
|
9
|
+
error?: Error;
|
|
10
|
+
};
|
|
11
|
+
export type ConnectAvatarSessionMediaOptions = {
|
|
12
|
+
/** Optional WHEP video target. If omitted, the control socket still connects and WS mux remains the media fallback. */
|
|
13
|
+
video?: HTMLVideoElement | null;
|
|
14
|
+
/** Keep the existing make-before-break fallback only. */
|
|
15
|
+
media?: "auto" | "off";
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
WebSocketImpl?: typeof WebSocket;
|
|
18
|
+
whep?: CloudflareWhepPlaybackOptions;
|
|
19
|
+
/** Default false: media-plane failures are reported but do not fail the realtime session. */
|
|
20
|
+
throwOnMediaError?: boolean;
|
|
21
|
+
/** Default false: resolve as soon as prepare succeeds; WHEP connects in the background. */
|
|
22
|
+
waitForMedia?: boolean;
|
|
23
|
+
/** Optional deadline used only when `waitForMedia` is true. */
|
|
24
|
+
waitForMediaMs?: number;
|
|
25
|
+
onMediaState?: (event: AvatarSessionMediaStateEvent) => void;
|
|
26
|
+
};
|
|
27
|
+
export type AvatarSessionMediaConnection = AvatarSessionConnectResult & {
|
|
28
|
+
media: RealtimeMediaPlan | null;
|
|
29
|
+
/** Resolves to the WHEP playback when available, or null when disabled/failed without strict media errors. */
|
|
30
|
+
mediaReady: Promise<CloudflareWhepPlayback | null>;
|
|
31
|
+
readonly mediaState: AvatarSessionMediaState;
|
|
32
|
+
readonly mediaPlayback: CloudflareWhepPlayback | null;
|
|
33
|
+
close: () => Promise<void>;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Browser-friendly session bootstrap: connect the low-latency control socket,
|
|
37
|
+
* attach Cloudflare WHEP as soon as the grant exists, then prepare the avatar.
|
|
38
|
+
* That lets SDP/ICE and Cloudflare viewer join latency overlap GPU warmup.
|
|
39
|
+
* WHEP never blocks the fallback canvas/mux path unless `waitForMedia` or
|
|
40
|
+
* `throwOnMediaError` is explicitly requested.
|
|
41
|
+
*/
|
|
42
|
+
export declare function connectAvatarSessionMedia(session: AvatarSession, options?: ConnectAvatarSessionMediaOptions): Promise<AvatarSessionMediaConnection>;
|
|
43
|
+
//# sourceMappingURL=media-session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-session.d.ts","sourceRoot":"","sources":["../../src/browser/media-session.ts"],"names":[],"mappings":"AACA,OAAO,EAA6B,KAAK,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7E,OAAO,KAAK,EAAE,aAAa,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC;AAC5E,OAAO,EAAgC,KAAK,sBAAsB,EAAE,KAAK,6BAA6B,EAAE,MAAM,UAAU,CAAC;AAEzH,MAAM,MAAM,uBAAuB,GAAG,UAAU,GAAG,aAAa,GAAG,YAAY,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AAEpH,MAAM,MAAM,4BAA4B,GAAG;IACzC,KAAK,EAAE,uBAAuB,CAAC;IAC/B,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,EAAE,sBAAsB,CAAC;IAClC,KAAK,CAAC,EAAE,KAAK,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,gCAAgC,GAAG;IAC7C,uHAAuH;IACvH,KAAK,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAChC,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACvB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,SAAS,CAAC;IACjC,IAAI,CAAC,EAAE,6BAA6B,CAAC;IACrC,6FAA6F;IAC7F,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,2FAA2F;IAC3F,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,+DAA+D;IAC/D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,4BAA4B,KAAK,IAAI,CAAC;CAC9D,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG,0BAA0B,GAAG;IACtE,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAChC,8GAA8G;IAC9G,UAAU,EAAE,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAAC;IACnD,QAAQ,CAAC,UAAU,EAAE,uBAAuB,CAAC;IAC7C,QAAQ,CAAC,aAAa,EAAE,sBAAsB,GAAG,IAAI,CAAC;IACtD,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B,CAAC;AAEF;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,aAAa,EACtB,OAAO,GAAE,gCAAqC,GAC7C,OAAO,CAAC,4BAA4B,CAAC,CA4EvC"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { isCloudflareWhepMediaPlan } from "../media";
|
|
2
|
+
import { attachCloudflareWhepPlayback } from "./webrtc";
|
|
3
|
+
/**
|
|
4
|
+
* Browser-friendly session bootstrap: connect the low-latency control socket,
|
|
5
|
+
* attach Cloudflare WHEP as soon as the grant exists, then prepare the avatar.
|
|
6
|
+
* That lets SDP/ICE and Cloudflare viewer join latency overlap GPU warmup.
|
|
7
|
+
* WHEP never blocks the fallback canvas/mux path unless `waitForMedia` or
|
|
8
|
+
* `throwOnMediaError` is explicitly requested.
|
|
9
|
+
*/
|
|
10
|
+
export async function connectAvatarSessionMedia(session, options = {}) {
|
|
11
|
+
let state = "disabled";
|
|
12
|
+
let playback = null;
|
|
13
|
+
let closed = false;
|
|
14
|
+
let media = null;
|
|
15
|
+
const mediaReadyRef = { current: null };
|
|
16
|
+
const mediaAbort = new AbortController();
|
|
17
|
+
const cleanupParentAbort = linkAbort(options.signal, mediaAbort);
|
|
18
|
+
const emit = (event) => {
|
|
19
|
+
state = event.state;
|
|
20
|
+
options.onMediaState?.(event);
|
|
21
|
+
};
|
|
22
|
+
const startMedia = (grant) => {
|
|
23
|
+
if (mediaReadyRef.current)
|
|
24
|
+
return;
|
|
25
|
+
media = grant.media ?? null;
|
|
26
|
+
mediaReadyRef.current = startMediaPlayback({
|
|
27
|
+
media,
|
|
28
|
+
video: options.video ?? null,
|
|
29
|
+
mode: options.media ?? "auto",
|
|
30
|
+
signal: mediaAbort.signal,
|
|
31
|
+
whep: options.whep,
|
|
32
|
+
throwOnMediaError: options.throwOnMediaError ?? false,
|
|
33
|
+
emit,
|
|
34
|
+
}).then((result) => {
|
|
35
|
+
if (closed && result)
|
|
36
|
+
void result.close();
|
|
37
|
+
if (!closed)
|
|
38
|
+
playback = result;
|
|
39
|
+
return closed ? null : result;
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
let connected;
|
|
43
|
+
try {
|
|
44
|
+
connected = await session.connectWithGrant({
|
|
45
|
+
signal: options.signal,
|
|
46
|
+
WebSocketImpl: options.WebSocketImpl,
|
|
47
|
+
onGrant: startMedia,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
closed = true;
|
|
52
|
+
cleanupParentAbort();
|
|
53
|
+
mediaAbort.abort();
|
|
54
|
+
const pendingMediaReady = mediaReadyRef.current;
|
|
55
|
+
const currentPlayback = playback ?? (pendingMediaReady ? await pendingMediaReady.catch(() => null) : null);
|
|
56
|
+
await currentPlayback?.close();
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
startMedia(connected.grant);
|
|
60
|
+
const ready = mediaReadyRef.current ?? Promise.resolve(null);
|
|
61
|
+
if (options.waitForMedia) {
|
|
62
|
+
await withOptionalTimeout(ready, options.waitForMediaMs ?? 8_000);
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
...connected,
|
|
66
|
+
media,
|
|
67
|
+
mediaReady: ready,
|
|
68
|
+
get mediaState() {
|
|
69
|
+
return state;
|
|
70
|
+
},
|
|
71
|
+
get mediaPlayback() {
|
|
72
|
+
return playback;
|
|
73
|
+
},
|
|
74
|
+
close: async () => {
|
|
75
|
+
if (closed)
|
|
76
|
+
return;
|
|
77
|
+
closed = true;
|
|
78
|
+
cleanupParentAbort();
|
|
79
|
+
mediaAbort.abort();
|
|
80
|
+
const currentPlayback = playback ?? (await ready.catch(() => null));
|
|
81
|
+
await currentPlayback?.close();
|
|
82
|
+
session.disconnect();
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async function startMediaPlayback(options) {
|
|
87
|
+
if (options.mode === "off" || !options.media) {
|
|
88
|
+
options.emit({ state: "disabled", media: options.media });
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
if (!isCloudflareWhepMediaPlan(options.media)) {
|
|
92
|
+
options.emit({ state: "disabled", media: options.media });
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
if (!options.video) {
|
|
96
|
+
options.emit({ state: "unsupported", media: options.media });
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
options.emit({ state: "connecting", media: options.media });
|
|
101
|
+
const playback = await attachCloudflareWhepPlayback(options.media, options.video, {
|
|
102
|
+
// Cloudflare WHEP may return 409 until the paired WHIP publisher has
|
|
103
|
+
// started sending media. Keep probing in the background; the mux/canvas
|
|
104
|
+
// path is already live and this promise is non-blocking by default.
|
|
105
|
+
retries: 20,
|
|
106
|
+
retryDelayMs: 500,
|
|
107
|
+
iceGatheringTimeoutMs: 800,
|
|
108
|
+
...options.whep,
|
|
109
|
+
signal: options.signal,
|
|
110
|
+
});
|
|
111
|
+
options.emit({ state: "connected", media: options.media, playback });
|
|
112
|
+
armFirstFrameState(options.video, options.signal, () => {
|
|
113
|
+
options.emit({ state: "playing", media: options.media, playback });
|
|
114
|
+
});
|
|
115
|
+
return playback;
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
119
|
+
options.emit({ state: "error", media: options.media, error: normalized });
|
|
120
|
+
if (options.throwOnMediaError)
|
|
121
|
+
throw normalized;
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function linkAbort(parent, child) {
|
|
126
|
+
if (!parent)
|
|
127
|
+
return () => undefined;
|
|
128
|
+
const abort = () => child.abort();
|
|
129
|
+
if (parent.aborted)
|
|
130
|
+
abort();
|
|
131
|
+
else
|
|
132
|
+
parent.addEventListener("abort", abort, { once: true });
|
|
133
|
+
return () => parent.removeEventListener("abort", abort);
|
|
134
|
+
}
|
|
135
|
+
function withOptionalTimeout(work, timeoutMs) {
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
const timer = setTimeout(() => reject(new Error("Realtime media did not become ready in time")), timeoutMs);
|
|
138
|
+
void work.then((value) => {
|
|
139
|
+
clearTimeout(timer);
|
|
140
|
+
resolve(value);
|
|
141
|
+
}, (error) => {
|
|
142
|
+
clearTimeout(timer);
|
|
143
|
+
reject(error);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function armFirstFrameState(video, signal, onFrame) {
|
|
148
|
+
if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
|
|
149
|
+
onFrame();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const events = ["loadeddata", "canplay", "playing", "timeupdate"];
|
|
153
|
+
const cleanup = () => {
|
|
154
|
+
for (const event of events)
|
|
155
|
+
video.removeEventListener(event, done);
|
|
156
|
+
signal.removeEventListener("abort", cleanup);
|
|
157
|
+
};
|
|
158
|
+
const done = () => {
|
|
159
|
+
cleanup();
|
|
160
|
+
onFrame();
|
|
161
|
+
};
|
|
162
|
+
for (const event of events)
|
|
163
|
+
video.addEventListener(event, done, { once: true });
|
|
164
|
+
signal.addEventListener("abort", cleanup, { once: true });
|
|
165
|
+
}
|
|
166
|
+
// Compile-time guard: SDK users can pass the exact grant from `createSessionGrant()`.
|
|
167
|
+
const _grantMediaPin = undefined;
|
|
168
|
+
void _grantMediaPin;
|
|
169
|
+
//# sourceMappingURL=media-session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-session.js","sourceRoot":"","sources":["../../src/browser/media-session.ts"],"names":[],"mappings":"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"}
|