@spatialwalk/avatarkit-rtc 1.0.0-beta.1 → 1.0.0-beta.10
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 +2 -410
- package/dist/assets/animation-worker-DOGeTjF0.js.map +1 -0
- package/dist/core/AvatarPlayer.d.ts +96 -12
- package/dist/core/AvatarPlayer.d.ts.map +1 -1
- package/dist/core/RTCProvider.d.ts +12 -16
- package/dist/core/RTCProvider.d.ts.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index10.js +91 -39
- package/dist/index10.js.map +1 -1
- package/dist/index11.js +14 -386
- package/dist/index11.js.map +1 -1
- package/dist/index12.js +350 -64
- package/dist/index12.js.map +1 -1
- package/dist/index13.js +44 -14
- package/dist/index13.js.map +1 -1
- package/dist/index14.js +25 -44
- package/dist/index14.js.map +1 -1
- package/dist/index2.js +335 -46
- package/dist/index2.js.map +1 -1
- package/dist/index3.js +265 -54
- package/dist/index3.js.map +1 -1
- package/dist/index4.js +105 -86
- package/dist/index4.js.map +1 -1
- package/dist/index5.js +6 -2
- package/dist/index5.js.map +1 -1
- package/dist/index6.js +603 -39
- package/dist/index6.js.map +1 -1
- package/dist/index8.js +128 -167
- package/dist/index8.js.map +1 -1
- package/dist/index9.js +65 -164
- package/dist/index9.js.map +1 -1
- package/dist/providers/agora/AgoraProvider.d.ts +0 -13
- package/dist/providers/agora/AgoraProvider.d.ts.map +1 -1
- package/dist/providers/agora/index.d.ts +1 -5
- package/dist/providers/agora/index.d.ts.map +1 -1
- package/dist/providers/agora/types.d.ts.map +1 -1
- package/dist/providers/base/BaseProvider.d.ts +50 -8
- package/dist/providers/base/BaseProvider.d.ts.map +1 -1
- package/dist/providers/livekit/LiveKitProvider.d.ts +4 -15
- package/dist/providers/livekit/LiveKitProvider.d.ts.map +1 -1
- package/dist/providers/livekit/animation-worker.d.ts.map +1 -1
- package/dist/providers/livekit/index.d.ts +1 -5
- package/dist/providers/livekit/index.d.ts.map +1 -1
- package/dist/types/index.d.ts +21 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/telemetry.d.ts +22 -0
- package/dist/utils/telemetry.d.ts.map +1 -0
- package/package.json +21 -12
- package/dist/assets/animation-worker-CUXZycUw.js.map +0 -1
- package/dist/index15.js +0 -29
- package/dist/index15.js.map +0 -1
- package/dist/index16.js +0 -144
- package/dist/index16.js.map +0 -1
- package/dist/index17.js +0 -106
- package/dist/index17.js.map +0 -1
- package/dist/index18.js +0 -28
- package/dist/index18.js.map +0 -1
- package/dist/proto/animation.d.ts +0 -12
- package/dist/proto/animation.d.ts.map +0 -1
package/dist/index14.js
CHANGED
|
@@ -1,48 +1,29 @@
|
|
|
1
|
-
import { logger } from "./index7.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
flags: event.data.flags
|
|
26
|
-
});
|
|
27
|
-
} else if (type === "transitionEnd") {
|
|
28
|
-
onEvent == null ? void 0 : onEvent({
|
|
29
|
-
type: "transitionEnd",
|
|
30
|
-
protobufData: event.data.protobufData,
|
|
31
|
-
flags: event.data.flags
|
|
32
|
-
});
|
|
33
|
-
} else if (type === "idleStart") {
|
|
34
|
-
onEvent == null ? void 0 : onEvent({ type: "idleStart" });
|
|
35
|
-
} else if (type === "error") {
|
|
36
|
-
logger.error("AnimationTransform", "Error:", event.data.error);
|
|
37
|
-
onEvent == null ? void 0 : onEvent(event.data);
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
worker.onerror = (event) => {
|
|
41
|
-
logger.error("AnimationTransform", "Worker error:", event.message);
|
|
42
|
-
};
|
|
43
|
-
return new RTCRtpScriptTransform(worker, { operation: "receiver" });
|
|
1
|
+
const jsContent = 'const VP8_FRAME_HEADER_SIZE = 10;\nconst METADATA_FIXED_HEADER_SIZE = 5;\nconst PACKET_FLAG_IDLE = 1;\nconst PACKET_FLAG_START = 2;\nconst PACKET_FLAG_END = 4;\nconst PACKET_FLAG_GZIPPED = 8;\nconst PACKET_FLAG_TRANSITION = 16;\nconst PACKET_FLAG_TRANSITION_END = 32;\nconst PACKET_FLAG_REDUNDANT = 64;\nlet receiverMetaCount = 0;\nlet lastLogTime = 0;\nlet totalFrames = 0;\nlet framesWithMeta = 0;\nlet wasIdle = false;\nlet isInStartTransition = false;\nlet isInEndTransition = false;\nlet lastReceivedTimestamp = null;\nlet framesRecovered = 0;\nlet framesLost = 0;\nconst EXPECTED_TIMESTAMP_INCREMENT = 3600;\nlet lastRenderedSeq = -1;\nlet framesOutOfOrder = 0;\nlet framesDuplicate = 0;\nlet framesDropped = 0;\nlet framesSent = 0;\nfunction parseMetadataHeader(data) {\n if (data.byteLength < METADATA_FIXED_HEADER_SIZE) return null;\n const view = new DataView(data.buffer, data.byteOffset, data.byteLength);\n const flags = data[0];\n const msgLen = view.getUint32(1, true);\n const headerSize = METADATA_FIXED_HEADER_SIZE + msgLen;\n if (data.byteLength < headerSize) return null;\n const compressedData = data.slice(\n METADATA_FIXED_HEADER_SIZE,\n METADATA_FIXED_HEADER_SIZE + msgLen\n );\n const hasRedundant = (flags & PACKET_FLAG_REDUNDANT) !== 0;\n return {\n meta: {\n flags,\n protobufLength: msgLen,\n protobufData: compressedData,\n // This is still compressed at this point\n hasRedundant,\n redundantLength: 0,\n // Will be determined after decompression\n redundantData: null\n // Will be extracted after decompression\n },\n headerSize\n };\n}\nfunction isIdlePacket(flags) {\n return (flags & PACKET_FLAG_IDLE) !== 0;\n}\nfunction isStartPacket(flags) {\n return (flags & PACKET_FLAG_START) !== 0;\n}\nfunction isEndPacket(flags) {\n return (flags & PACKET_FLAG_END) !== 0;\n}\nfunction isGzipped(flags) {\n return (flags & PACKET_FLAG_GZIPPED) !== 0;\n}\nfunction isTransitionPacket(flags) {\n return (flags & PACKET_FLAG_TRANSITION) !== 0;\n}\nfunction isTransitionEndPacket(flags) {\n return (flags & PACKET_FLAG_TRANSITION_END) !== 0;\n}\nasync function decompressGzip(data) {\n const ds = new DecompressionStream("gzip");\n const writer = ds.writable.getWriter();\n const copy = new Uint8Array(data);\n writer.write(copy);\n writer.close();\n const reader = ds.readable.getReader();\n const chunks = [];\n let totalLength = 0;\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n totalLength += value.length;\n }\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const chunk of chunks) {\n result.set(chunk, offset);\n offset += chunk.length;\n }\n return result;\n}\nlet totalCompressedBytes = 0;\nlet totalUncompressedBytes = 0;\nfunction parseALRPayload(decompressed, hasRedundant) {\n if (decompressed.byteLength < 4) return null;\n const view = new DataView(\n decompressed.buffer,\n decompressed.byteOffset,\n decompressed.byteLength\n );\n let offset = 0;\n const frameSeq = view.getUint32(offset, true);\n offset += 4;\n if (!hasRedundant) {\n const currentData2 = decompressed.slice(offset);\n return { frameSeq, currentData: currentData2, prev1Data: null, prev2Data: null };\n }\n if (decompressed.byteLength < offset + 4) return null;\n const currentLen = view.getUint32(offset, true);\n offset += 4;\n if (decompressed.byteLength < offset + currentLen) return null;\n const currentData = decompressed.slice(offset, offset + currentLen);\n offset += currentLen;\n let prev1Data = null;\n if (decompressed.byteLength >= offset + 4) {\n const prev1Len = view.getUint32(offset, true);\n offset += 4;\n if (prev1Len > 0 && decompressed.byteLength >= offset + prev1Len) {\n prev1Data = decompressed.slice(offset, offset + prev1Len);\n offset += prev1Len;\n }\n }\n let prev2Data = null;\n if (decompressed.byteLength >= offset + 4) {\n const prev2Len = view.getUint32(offset, true);\n offset += 4;\n if (prev2Len > 0 && decompressed.byteLength >= offset + prev2Len) {\n prev2Data = decompressed.slice(offset, offset + prev2Len);\n }\n }\n return { frameSeq, currentData, prev1Data, prev2Data };\n}\nfunction sendAnimationToMainThread(protobufData, flags, frameSeq, isRecovered = false) {\n const isIdle = isIdlePacket(flags);\n const isStart = isStartPacket(flags);\n const isEnd = isEndPacket(flags);\n if (frameSeq <= lastRenderedSeq && lastRenderedSeq !== -1 && !isStart) {\n if (frameSeq === lastRenderedSeq) {\n framesDuplicate++;\n } else {\n framesOutOfOrder++;\n }\n return false;\n }\n if (lastRenderedSeq !== -1 && frameSeq > lastRenderedSeq + 1 && !isStart) {\n const gap = frameSeq - lastRenderedSeq - 1;\n framesDropped += gap;\n }\n framesSent++;\n lastRenderedSeq = frameSeq;\n const protobufBuffer = new ArrayBuffer(protobufData.byteLength);\n new Uint8Array(protobufBuffer).set(protobufData);\n self.postMessage(\n {\n type: "animation",\n flags,\n isIdle,\n isStart,\n isEnd,\n isRecovered,\n frameSeq,\n protobufData: protobufBuffer\n },\n { transfer: [protobufBuffer] }\n );\n return true;\n}\nfunction receiverTransform(frame, _controller) {\n totalFrames++;\n const data = new Uint8Array(frame.data);\n const currentTimestamp = frame.timestamp;\n if (data.length <= VP8_FRAME_HEADER_SIZE) {\n return;\n }\n const animData = data.subarray(VP8_FRAME_HEADER_SIZE);\n const parsed = parseMetadataHeader(animData);\n if (parsed) {\n const { meta } = parsed;\n framesWithMeta++;\n receiverMetaCount++;\n const isIdle = isIdlePacket(meta.flags);\n const isStart = isStartPacket(meta.flags);\n const isEnd = isEndPacket(meta.flags);\n if (lastReceivedTimestamp !== null && !isIdle && !isStart) {\n const timestampDelta = currentTimestamp - lastReceivedTimestamp;\n if (timestampDelta > EXPECTED_TIMESTAMP_INCREMENT * 1.5) {\n const missedFrames = Math.round(timestampDelta / EXPECTED_TIMESTAMP_INCREMENT) - 1;\n framesLost += missedFrames;\n if (meta.hasRedundant && isGzipped(meta.flags)) {\n totalCompressedBytes += meta.protobufData.byteLength;\n decompressGzip(meta.protobufData).then((decompressed) => {\n totalUncompressedBytes += decompressed.byteLength;\n const parsed2 = parseALRPayload(decompressed, true);\n if (parsed2) {\n const currentSeq = parsed2.frameSeq;\n const framesToRecover = [];\n if (missedFrames >= 2 && parsed2.prev2Data) {\n framesToRecover.push({\n data: parsed2.prev2Data,\n seq: currentSeq - 2\n });\n }\n if (missedFrames >= 1 && parsed2.prev1Data) {\n framesToRecover.push({\n data: parsed2.prev1Data,\n seq: currentSeq - 1\n });\n }\n const recovered = framesToRecover.length;\n if (recovered > 0) {\n framesRecovered += recovered;\n for (const frame2 of framesToRecover) {\n sendAnimationToMainThread(\n frame2.data,\n meta.flags & ~PACKET_FLAG_REDUNDANT,\n frame2.seq,\n true\n );\n }\n }\n sendAnimationToMainThread(\n parsed2.currentData,\n meta.flags & ~PACKET_FLAG_REDUNDANT,\n currentSeq,\n false\n );\n }\n }).catch((err) => {\n console.error(`[Animation Worker] ALR decompression error:`, err);\n });\n lastReceivedTimestamp = currentTimestamp;\n return;\n }\n }\n }\n if (!isIdle) {\n lastReceivedTimestamp = currentTimestamp;\n }\n if (isStart) {\n lastReceivedTimestamp = currentTimestamp;\n framesRecovered = 0;\n framesLost = 0;\n lastRenderedSeq = -1;\n framesOutOfOrder = 0;\n framesDuplicate = 0;\n framesDropped = 0;\n framesSent = 0;\n }\n const isTransition = isTransitionPacket(meta.flags);\n const isTransitionEnd = isTransitionEndPacket(meta.flags);\n if (isIdle) {\n if (!wasIdle) {\n self.postMessage({ type: "idleStart" });\n wasIdle = true;\n }\n isInStartTransition = false;\n isInEndTransition = false;\n } else if (isTransition && meta.protobufLength > 0) {\n wasIdle = false;\n if (!isInStartTransition) {\n isInStartTransition = true;\n isInEndTransition = false;\n const gzipped = isGzipped(meta.flags);\n if (gzipped) {\n totalCompressedBytes += meta.protobufData.byteLength;\n decompressGzip(meta.protobufData).then((decompressed) => {\n totalUncompressedBytes += decompressed.byteLength;\n const protobufBuffer = new ArrayBuffer(decompressed.byteLength);\n new Uint8Array(protobufBuffer).set(decompressed);\n self.postMessage(\n {\n type: "transition",\n flags: meta.flags,\n protobufData: protobufBuffer\n },\n { transfer: [protobufBuffer] }\n );\n }).catch((err) => {\n console.error(\n `[Animation Worker] Gzip decompress error (transition):`,\n err\n );\n });\n } else {\n const protobufBuffer = new ArrayBuffer(meta.protobufData.byteLength);\n new Uint8Array(protobufBuffer).set(meta.protobufData);\n self.postMessage(\n {\n type: "transition",\n flags: meta.flags,\n protobufData: protobufBuffer\n },\n { transfer: [protobufBuffer] }\n );\n }\n }\n } else if (isTransitionEnd && meta.protobufLength > 0) {\n if (!isInEndTransition) {\n isInEndTransition = true;\n isInStartTransition = false;\n const gzipped = isGzipped(meta.flags);\n if (gzipped) {\n totalCompressedBytes += meta.protobufData.byteLength;\n decompressGzip(meta.protobufData).then((decompressed) => {\n totalUncompressedBytes += decompressed.byteLength;\n const protobufBuffer = new ArrayBuffer(decompressed.byteLength);\n new Uint8Array(protobufBuffer).set(decompressed);\n self.postMessage(\n {\n type: "transitionEnd",\n flags: meta.flags,\n protobufData: protobufBuffer\n },\n { transfer: [protobufBuffer] }\n );\n }).catch((err) => {\n console.error(\n `[Animation Worker] Gzip decompress error (transitionEnd):`,\n err\n );\n });\n } else {\n const protobufBuffer = new ArrayBuffer(meta.protobufData.byteLength);\n new Uint8Array(protobufBuffer).set(meta.protobufData);\n self.postMessage(\n {\n type: "transitionEnd",\n flags: meta.flags,\n protobufData: protobufBuffer\n },\n { transfer: [protobufBuffer] }\n );\n }\n }\n } else if (meta.protobufLength > 0) {\n if (wasIdle) {\n wasIdle = false;\n }\n isInStartTransition = false;\n isInEndTransition = false;\n const gzipped = isGzipped(meta.flags);\n if (gzipped) {\n totalCompressedBytes += meta.protobufData.byteLength;\n decompressGzip(meta.protobufData).then((decompressed) => {\n totalUncompressedBytes += decompressed.byteLength;\n const parsed2 = parseALRPayload(decompressed, meta.hasRedundant);\n if (parsed2) {\n sendAnimationToMainThread(\n parsed2.currentData,\n meta.flags & ~PACKET_FLAG_REDUNDANT,\n parsed2.frameSeq,\n false\n );\n }\n }).catch(() => {\n });\n } else {\n const protobufBuffer = new ArrayBuffer(meta.protobufData.byteLength);\n new Uint8Array(protobufBuffer).set(meta.protobufData);\n self.postMessage(\n {\n type: "animation",\n flags: meta.flags,\n isIdle: false,\n isStart,\n isEnd,\n frameSeq: -1,\n // Unknown sequence\n protobufData: protobufBuffer\n },\n { transfer: [protobufBuffer] }\n );\n }\n }\n const now = performance.now();\n if (now - lastLogTime > 1e3) {\n lastLogTime = now;\n self.postMessage({\n type: "metadata",\n protobufLength: meta.protobufLength,\n framesPerSec: receiverMetaCount,\n totalFrames,\n framesWithMeta,\n framesLost,\n framesRecovered,\n totalCompressedBytes,\n totalUncompressedBytes,\n framesOutOfOrder,\n framesDuplicate,\n framesDropped,\n framesSent,\n lastRenderedSeq\n });\n receiverMetaCount = 0;\n }\n }\n}\nself.onrtctransform = (event) => {\n const transformer = event.transformer;\n const options = transformer.options;\n try {\n if (options.operation === "receiver") {\n receiverMetaCount = 0;\n lastLogTime = 0;\n totalFrames = 0;\n framesWithMeta = 0;\n wasIdle = false;\n isInStartTransition = false;\n isInEndTransition = false;\n lastReceivedTimestamp = null;\n framesRecovered = 0;\n framesLost = 0;\n lastRenderedSeq = -1;\n framesOutOfOrder = 0;\n framesDuplicate = 0;\n framesDropped = 0;\n framesSent = 0;\n transformer.readable.pipeThrough(new TransformStream({ transform: receiverTransform })).pipeTo(transformer.writable).catch((err) => {\n console.error("[Animation Worker] Pipeline error:", err);\n self.postMessage({\n type: "error",\n error: `Animation receiver pipe error: ${err}`\n });\n });\n self.postMessage({ type: "ready", operation: "receiver" });\n }\n } catch (err) {\n console.error("[Animation Worker] Setup error:", err);\n self.postMessage({\n type: "error",\n error: `Animation transform setup error: ${err}`\n });\n }\n};\nself.onmessage = (event) => {\n const { type } = event.data;\n if (type === "init") {\n self.postMessage({ type: "initialized" });\n }\n};\n//# sourceMappingURL=animation-worker-DOGeTjF0.js.map\n';
|
|
2
|
+
const blob = typeof self !== "undefined" && self.Blob && new Blob(["URL.revokeObjectURL(import.meta.url);", jsContent], { type: "text/javascript;charset=utf-8" });
|
|
3
|
+
function WorkerWrapper(options) {
|
|
4
|
+
let objURL;
|
|
5
|
+
try {
|
|
6
|
+
objURL = blob && (self.URL || self.webkitURL).createObjectURL(blob);
|
|
7
|
+
if (!objURL) throw "";
|
|
8
|
+
const worker = new Worker(objURL, {
|
|
9
|
+
type: "module",
|
|
10
|
+
name: options == null ? void 0 : options.name
|
|
11
|
+
});
|
|
12
|
+
worker.addEventListener("error", () => {
|
|
13
|
+
(self.URL || self.webkitURL).revokeObjectURL(objURL);
|
|
14
|
+
});
|
|
15
|
+
return worker;
|
|
16
|
+
} catch (e) {
|
|
17
|
+
return new Worker(
|
|
18
|
+
"data:text/javascript;charset=utf-8," + encodeURIComponent(jsContent),
|
|
19
|
+
{
|
|
20
|
+
type: "module",
|
|
21
|
+
name: options == null ? void 0 : options.name
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
}
|
|
44
25
|
}
|
|
45
26
|
export {
|
|
46
|
-
|
|
27
|
+
WorkerWrapper as default
|
|
47
28
|
};
|
|
48
29
|
//# sourceMappingURL=index14.js.map
|
package/dist/index14.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index14.js","sources":[
|
|
1
|
+
{"version":3,"file":"index14.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/index2.js
CHANGED
|
@@ -2,7 +2,8 @@ var __defProp = Object.defineProperty;
|
|
|
2
2
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
3
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
4
|
import { AnimationHandler } from "./index6.js";
|
|
5
|
-
import { configureLogger } from "./index7.js";
|
|
5
|
+
import { configureLogger, logger } from "./index7.js";
|
|
6
|
+
import { initializeTelemetry, trackEvent, flushTelemetry } from "./index8.js";
|
|
6
7
|
class AvatarPlayer {
|
|
7
8
|
/**
|
|
8
9
|
* Create a new AvatarPlayer instance.
|
|
@@ -22,14 +23,53 @@ class AvatarPlayer {
|
|
|
22
23
|
/** @internal */
|
|
23
24
|
__publicField(this, "localAudioTrack", null);
|
|
24
25
|
/** @internal */
|
|
25
|
-
__publicField(this, "
|
|
26
|
+
__publicField(this, "microphoneStream", null);
|
|
27
|
+
/** @internal */
|
|
28
|
+
__publicField(this, "eventHandlers", /* @__PURE__ */ new Map());
|
|
29
|
+
/** @internal */
|
|
30
|
+
__publicField(this, "lastConnectionConfig", null);
|
|
31
|
+
/** @internal */
|
|
32
|
+
__publicField(this, "isReconnecting", false);
|
|
33
|
+
/** @internal */
|
|
34
|
+
__publicField(this, "hasActiveAnimationSession", false);
|
|
35
|
+
/** @internal */
|
|
36
|
+
__publicField(this, "isTrackingPrimed", false);
|
|
37
|
+
/** @internal */
|
|
38
|
+
__publicField(this, "connectStartTime", 0);
|
|
39
|
+
/** @internal */
|
|
40
|
+
__publicField(this, "sessionStartTime", 0);
|
|
41
|
+
/** @internal */
|
|
42
|
+
__publicField(this, "stallCount", 0);
|
|
43
|
+
/** @internal */
|
|
44
|
+
__publicField(this, "reconnectCount", 0);
|
|
45
|
+
/** @internal */
|
|
46
|
+
__publicField(this, "conversationCount", 0);
|
|
47
|
+
/** @internal */
|
|
48
|
+
__publicField(this, "providerName", "");
|
|
26
49
|
this.provider = provider;
|
|
27
50
|
this.avatarView = avatarView;
|
|
51
|
+
this.providerName = provider.name;
|
|
28
52
|
if ((options == null ? void 0 : options.logLevel) !== void 0) {
|
|
29
53
|
configureLogger({ level: options.logLevel });
|
|
30
54
|
}
|
|
55
|
+
initializeTelemetry({ sdkVersion: "1.0.0-beta.10" });
|
|
56
|
+
logger.info("AvatarPlayer", `SDK version: ${"1.0.0-beta.10"}`);
|
|
31
57
|
const renderer = this.createRendererAdapter();
|
|
32
|
-
this.animationHandler = new AnimationHandler(renderer
|
|
58
|
+
this.animationHandler = new AnimationHandler(renderer, {
|
|
59
|
+
enableJitterBuffer: options == null ? void 0 : options.enableJitterBuffer,
|
|
60
|
+
maxBufferDelayMs: options == null ? void 0 : options.maxBufferDelayMs,
|
|
61
|
+
providerName: this.providerName,
|
|
62
|
+
onStreamStalled: () => {
|
|
63
|
+
this.hasActiveAnimationSession = false;
|
|
64
|
+
this.isTrackingPrimed = false;
|
|
65
|
+
this.stallCount++;
|
|
66
|
+
trackEvent("rtc_stream_stalled", "warning", {
|
|
67
|
+
provider: this.providerName,
|
|
68
|
+
session_elapsed: this.sessionStartTime ? Date.now() - this.sessionStartTime : 0
|
|
69
|
+
});
|
|
70
|
+
this.emit("stalled");
|
|
71
|
+
}
|
|
72
|
+
});
|
|
33
73
|
this.setupProviderEvents();
|
|
34
74
|
}
|
|
35
75
|
/**
|
|
@@ -47,9 +87,39 @@ class AvatarPlayer {
|
|
|
47
87
|
if (this._isConnected) {
|
|
48
88
|
throw new Error("Already connected. Please disconnect first.");
|
|
49
89
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
90
|
+
this.lastConnectionConfig = config;
|
|
91
|
+
this.connectStartTime = performance.now();
|
|
92
|
+
trackEvent("rtc_connect_start", "info", {
|
|
93
|
+
provider: this.providerName
|
|
94
|
+
});
|
|
95
|
+
try {
|
|
96
|
+
await this.setupAnimationCallbacks();
|
|
97
|
+
await this.provider.connect(config);
|
|
98
|
+
this._isConnected = true;
|
|
99
|
+
this.sessionStartTime = Date.now();
|
|
100
|
+
this.stallCount = 0;
|
|
101
|
+
this.reconnectCount = 0;
|
|
102
|
+
this.conversationCount = 0;
|
|
103
|
+
trackEvent("rtc_connect_success", "info", {
|
|
104
|
+
provider: this.providerName,
|
|
105
|
+
duration: Math.round(performance.now() - this.connectStartTime)
|
|
106
|
+
});
|
|
107
|
+
} catch (error) {
|
|
108
|
+
trackEvent("rtc_connect_failed", "error", {
|
|
109
|
+
provider: this.providerName,
|
|
110
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
111
|
+
duration: Math.round(performance.now() - this.connectStartTime)
|
|
112
|
+
});
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Pre-warm a future RTC connection when the provider supports it.
|
|
118
|
+
* This is best-effort and does not change the connected state.
|
|
119
|
+
*/
|
|
120
|
+
async prepareConnection(config) {
|
|
121
|
+
var _a, _b;
|
|
122
|
+
await ((_b = (_a = this.provider).prepareConnection) == null ? void 0 : _b.call(_a, config));
|
|
53
123
|
}
|
|
54
124
|
/**
|
|
55
125
|
* Disconnect from RTC server.
|
|
@@ -59,40 +129,132 @@ class AvatarPlayer {
|
|
|
59
129
|
if (!this._isConnected) {
|
|
60
130
|
return;
|
|
61
131
|
}
|
|
62
|
-
if (this.
|
|
132
|
+
if (this.microphoneStream) {
|
|
63
133
|
await this.stopPublishing();
|
|
134
|
+
} else if (this.localAudioTrack) {
|
|
135
|
+
await this.unpublishAudio();
|
|
64
136
|
}
|
|
137
|
+
const sessionSummary = this.animationHandler.getSessionSummary();
|
|
65
138
|
await this.provider.disconnect();
|
|
66
139
|
this.animationHandler.dispose();
|
|
67
140
|
this._isConnected = false;
|
|
141
|
+
const sessionDuration = this.sessionStartTime ? Date.now() - this.sessionStartTime : 0;
|
|
142
|
+
trackEvent("rtc_session_summary", "info", {
|
|
143
|
+
provider: this.providerName,
|
|
144
|
+
total_duration_ms: sessionDuration,
|
|
145
|
+
total_frames: sessionSummary.totalFrames,
|
|
146
|
+
total_lost: sessionSummary.totalLost,
|
|
147
|
+
total_recovered: sessionSummary.totalRecovered,
|
|
148
|
+
total_dropped: sessionSummary.totalDropped,
|
|
149
|
+
avg_fps: sessionSummary.avgFps,
|
|
150
|
+
stall_count: this.stallCount,
|
|
151
|
+
reconnect_count: this.reconnectCount,
|
|
152
|
+
conversation_count: this.conversationCount
|
|
153
|
+
});
|
|
154
|
+
trackEvent("rtc_disconnected", "info", {
|
|
155
|
+
provider: this.providerName,
|
|
156
|
+
session_duration: sessionDuration
|
|
157
|
+
});
|
|
158
|
+
flushTelemetry();
|
|
68
159
|
}
|
|
69
160
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
161
|
+
* Publish an audio track to the RTC server.
|
|
162
|
+
*
|
|
163
|
+
* The audio source is controlled externally - you can pass any audio track:
|
|
164
|
+
* - Microphone: `getUserMedia({ audio: true })`
|
|
165
|
+
* - Browser audio: `audioElement.captureStream()`
|
|
166
|
+
* - Web Audio API: `audioContext.createMediaStreamDestination().stream`
|
|
167
|
+
* - Screen share audio: `getDisplayMedia({ audio: true })`
|
|
168
|
+
*
|
|
169
|
+
* @param track - MediaStreamTrack to publish (must be an audio track)
|
|
170
|
+
* @throws Error if not connected or track is invalid
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```typescript
|
|
174
|
+
* // Microphone
|
|
175
|
+
* const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
176
|
+
* await player.publishAudio(stream.getAudioTracks()[0]);
|
|
177
|
+
*
|
|
178
|
+
* // Browser audio element
|
|
179
|
+
* const audioEl = document.querySelector('audio');
|
|
180
|
+
* const stream = audioEl.captureStream();
|
|
181
|
+
* await player.publishAudio(stream.getAudioTracks()[0]);
|
|
182
|
+
* ```
|
|
73
183
|
*/
|
|
74
|
-
async
|
|
184
|
+
async publishAudio(track) {
|
|
185
|
+
if (!this._isConnected) {
|
|
186
|
+
throw new Error("Not connected. Please call connect() first.");
|
|
187
|
+
}
|
|
188
|
+
if (track.kind !== "audio") {
|
|
189
|
+
throw new Error("Invalid track: expected audio track");
|
|
190
|
+
}
|
|
75
191
|
if (this.localAudioTrack) {
|
|
76
|
-
|
|
192
|
+
await this.unpublishAudio();
|
|
193
|
+
}
|
|
194
|
+
this.localAudioTrack = track;
|
|
195
|
+
try {
|
|
196
|
+
await this.provider.publishAudioTrack(track);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
this.localAudioTrack = null;
|
|
199
|
+
trackEvent("rtc_audio_publish_failed", "error", {
|
|
200
|
+
provider: this.providerName,
|
|
201
|
+
description: error instanceof Error ? error.message : String(error)
|
|
202
|
+
});
|
|
203
|
+
throw error;
|
|
77
204
|
}
|
|
78
|
-
this.mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
79
|
-
this.localAudioTrack = this.mediaStream.getAudioTracks()[0];
|
|
80
|
-
await this.provider.publishAudioTrack(this.localAudioTrack);
|
|
81
205
|
}
|
|
82
206
|
/**
|
|
83
|
-
* Stop publishing
|
|
207
|
+
* Stop publishing audio.
|
|
208
|
+
* Note: This does NOT stop the track - the caller is responsible for managing the track lifecycle.
|
|
84
209
|
*/
|
|
85
|
-
async
|
|
210
|
+
async unpublishAudio() {
|
|
86
211
|
if (!this.localAudioTrack) {
|
|
87
212
|
return;
|
|
88
213
|
}
|
|
89
214
|
await this.provider.unpublishAudioTrack();
|
|
90
|
-
this.localAudioTrack.stop();
|
|
91
215
|
this.localAudioTrack = null;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Start publishing microphone audio.
|
|
219
|
+
*
|
|
220
|
+
* This requests microphone permission and publishes it to the RTC session.
|
|
221
|
+
* For custom audio sources (e.g., audio files), use `publishAudio(track)` instead.
|
|
222
|
+
*
|
|
223
|
+
* @throws Error if not connected, permission denied, or no microphone found
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* ```typescript
|
|
227
|
+
* await player.startPublishing();
|
|
228
|
+
* // ... later
|
|
229
|
+
* await player.stopPublishing();
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
232
|
+
async startPublishing() {
|
|
233
|
+
if (!this._isConnected) {
|
|
234
|
+
throw new Error("Not connected. Please call connect() first.");
|
|
95
235
|
}
|
|
236
|
+
if (this.microphoneStream) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
this.microphoneStream = await navigator.mediaDevices.getUserMedia({
|
|
240
|
+
audio: true
|
|
241
|
+
});
|
|
242
|
+
const track = this.microphoneStream.getAudioTracks()[0];
|
|
243
|
+
await this.publishAudio(track);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Stop publishing microphone audio and release the microphone.
|
|
247
|
+
*
|
|
248
|
+
* This stops the microphone track and releases the device.
|
|
249
|
+
* If you used `publishAudio()` with a custom track, use `unpublishAudio()` instead.
|
|
250
|
+
*/
|
|
251
|
+
async stopPublishing() {
|
|
252
|
+
if (!this.microphoneStream) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
await this.unpublishAudio();
|
|
256
|
+
this.microphoneStream.getTracks().forEach((track) => track.stop());
|
|
257
|
+
this.microphoneStream = null;
|
|
96
258
|
}
|
|
97
259
|
/**
|
|
98
260
|
* Get current connection state.
|
|
@@ -103,19 +265,19 @@ class AvatarPlayer {
|
|
|
103
265
|
}
|
|
104
266
|
/**
|
|
105
267
|
* Get the native RTC client object.
|
|
106
|
-
*
|
|
268
|
+
*
|
|
107
269
|
* Returns the underlying RTC client for advanced use cases:
|
|
108
270
|
* - LiveKit: Returns the Room instance
|
|
109
271
|
* - Agora: Returns the IAgoraRTCClient instance
|
|
110
|
-
*
|
|
272
|
+
*
|
|
111
273
|
* @returns The native RTC client object, or null if not connected
|
|
112
|
-
*
|
|
274
|
+
*
|
|
113
275
|
* @example
|
|
114
276
|
* ```typescript
|
|
115
277
|
* // Access LiveKit Room
|
|
116
278
|
* const room = player.getNativeClient() as Room;
|
|
117
279
|
* console.log('Participants:', room?.remoteParticipants.size);
|
|
118
|
-
*
|
|
280
|
+
*
|
|
119
281
|
* // Access Agora Client
|
|
120
282
|
* const client = player.getNativeClient() as IAgoraRTCClient;
|
|
121
283
|
* console.log('State:', client?.connectionState);
|
|
@@ -126,11 +288,18 @@ class AvatarPlayer {
|
|
|
126
288
|
}
|
|
127
289
|
/**
|
|
128
290
|
* Add event listener.
|
|
129
|
-
* @param event - Event name ('connected', 'disconnected', 'error', etc.)
|
|
291
|
+
* @param event - Event name ('connected', 'disconnected', 'error', 'stalled', etc.)
|
|
130
292
|
* @param handler - Event handler function
|
|
131
293
|
*/
|
|
132
294
|
on(event, handler) {
|
|
133
|
-
|
|
295
|
+
if (event === "stalled") {
|
|
296
|
+
if (!this.eventHandlers.has(event)) {
|
|
297
|
+
this.eventHandlers.set(event, /* @__PURE__ */ new Set());
|
|
298
|
+
}
|
|
299
|
+
this.eventHandlers.get(event).add(handler);
|
|
300
|
+
} else {
|
|
301
|
+
this.provider.on(event, handler);
|
|
302
|
+
}
|
|
134
303
|
}
|
|
135
304
|
/**
|
|
136
305
|
* Remove event listener.
|
|
@@ -138,7 +307,79 @@ class AvatarPlayer {
|
|
|
138
307
|
* @param handler - Event handler function (must be same reference as added)
|
|
139
308
|
*/
|
|
140
309
|
off(event, handler) {
|
|
141
|
-
|
|
310
|
+
var _a;
|
|
311
|
+
if (event === "stalled") {
|
|
312
|
+
(_a = this.eventHandlers.get(event)) == null ? void 0 : _a.delete(handler);
|
|
313
|
+
} else {
|
|
314
|
+
this.provider.off(event, handler);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Emit an event to local listeners.
|
|
319
|
+
* @internal
|
|
320
|
+
*/
|
|
321
|
+
emit(event, ...args) {
|
|
322
|
+
const handlers = this.eventHandlers.get(event);
|
|
323
|
+
if (handlers) {
|
|
324
|
+
handlers.forEach((handler) => {
|
|
325
|
+
try {
|
|
326
|
+
handler(...args);
|
|
327
|
+
} catch (e) {
|
|
328
|
+
logger.error("AvatarPlayer", `Error in ${event} handler:`, e);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Reconnect to RTC server using the last connection configuration.
|
|
335
|
+
* Useful for recovering from connection issues or stream stalls.
|
|
336
|
+
*
|
|
337
|
+
* @example
|
|
338
|
+
* ```typescript
|
|
339
|
+
* player.on('stalled', async () => {
|
|
340
|
+
* console.log('Stream stalled, attempting reconnection...');
|
|
341
|
+
* await player.reconnect();
|
|
342
|
+
* });
|
|
343
|
+
* ```
|
|
344
|
+
*
|
|
345
|
+
* @throws Error if no previous connection config exists or reconnection fails
|
|
346
|
+
*/
|
|
347
|
+
async reconnect() {
|
|
348
|
+
if (this.isReconnecting) {
|
|
349
|
+
logger.warn("AvatarPlayer", "Already attempting reconnection, skipping");
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (!this.lastConnectionConfig) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
"Cannot reconnect: no previous connection. Call connect() first."
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
this.isReconnecting = true;
|
|
358
|
+
this.reconnectCount++;
|
|
359
|
+
logger.info("AvatarPlayer", "Attempting reconnection...");
|
|
360
|
+
const reconnectStart = performance.now();
|
|
361
|
+
trackEvent("rtc_reconnect_start", "info", { provider: this.providerName });
|
|
362
|
+
try {
|
|
363
|
+
if (this._isConnected) {
|
|
364
|
+
await this.disconnect();
|
|
365
|
+
}
|
|
366
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
367
|
+
await this.connect(this.lastConnectionConfig);
|
|
368
|
+
logger.info("AvatarPlayer", "Reconnection successful");
|
|
369
|
+
trackEvent("rtc_reconnect_success", "info", {
|
|
370
|
+
provider: this.providerName,
|
|
371
|
+
duration: Math.round(performance.now() - reconnectStart)
|
|
372
|
+
});
|
|
373
|
+
} catch (error) {
|
|
374
|
+
logger.error("AvatarPlayer", "Reconnection failed:", error);
|
|
375
|
+
trackEvent("rtc_reconnect_failed", "error", {
|
|
376
|
+
provider: this.providerName,
|
|
377
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
378
|
+
});
|
|
379
|
+
throw error;
|
|
380
|
+
} finally {
|
|
381
|
+
this.isReconnecting = false;
|
|
382
|
+
}
|
|
142
383
|
}
|
|
143
384
|
/**
|
|
144
385
|
* Create internal renderer adapter that bridges AnimationHandler to AvatarView.
|
|
@@ -147,20 +388,19 @@ class AvatarPlayer {
|
|
|
147
388
|
createRendererAdapter() {
|
|
148
389
|
const avatarView = this.avatarView;
|
|
149
390
|
return {
|
|
150
|
-
|
|
391
|
+
renderFromProtobuf(protobufData) {
|
|
392
|
+
avatarView.renderFromProtobuf(protobufData);
|
|
393
|
+
},
|
|
394
|
+
renderFrame(frame, startIdle) {
|
|
395
|
+
const view = avatarView;
|
|
151
396
|
if (startIdle) {
|
|
152
|
-
|
|
153
|
-
} else if (
|
|
154
|
-
|
|
397
|
+
view.renderFlame(void 0, true);
|
|
398
|
+
} else if (frame) {
|
|
399
|
+
view.renderFlame(frame);
|
|
155
400
|
}
|
|
156
401
|
},
|
|
157
|
-
async
|
|
158
|
-
return avatarView.
|
|
159
|
-
to: targetFrame,
|
|
160
|
-
frameCount,
|
|
161
|
-
useLinear: true
|
|
162
|
-
// Linear interpolation for idle->speaking
|
|
163
|
-
});
|
|
402
|
+
async generateTransitionFromProtobuf(protobufData, frameCount) {
|
|
403
|
+
return avatarView.generateTransitionFromProtobuf(protobufData, frameCount);
|
|
164
404
|
},
|
|
165
405
|
isReady() {
|
|
166
406
|
return true;
|
|
@@ -174,9 +414,23 @@ class AvatarPlayer {
|
|
|
174
414
|
async setupAnimationCallbacks() {
|
|
175
415
|
const callbacks = {
|
|
176
416
|
onAnimationData: (protobufData, metadata) => {
|
|
177
|
-
if (
|
|
178
|
-
this.
|
|
179
|
-
|
|
417
|
+
if (!this.hasActiveAnimationSession) {
|
|
418
|
+
if (!this.isTrackingPrimed) {
|
|
419
|
+
this.animationHandler.resetTracking();
|
|
420
|
+
}
|
|
421
|
+
this.hasActiveAnimationSession = true;
|
|
422
|
+
this.isTrackingPrimed = false;
|
|
423
|
+
if (!metadata.isStart) {
|
|
424
|
+
logger.info(
|
|
425
|
+
"AvatarPlayer",
|
|
426
|
+
`Session start inferred from frame seq=${metadata.frameSeq ?? "n/a"}`
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
} else if (metadata.isStart) {
|
|
430
|
+
logger.info(
|
|
431
|
+
"AvatarPlayer",
|
|
432
|
+
`Ignoring duplicate session start frame seq=${metadata.frameSeq ?? "n/a"}`
|
|
433
|
+
);
|
|
180
434
|
}
|
|
181
435
|
this.animationHandler.handleAnimationData(
|
|
182
436
|
protobufData,
|
|
@@ -185,20 +439,44 @@ class AvatarPlayer {
|
|
|
185
439
|
);
|
|
186
440
|
},
|
|
187
441
|
onTransition: (protobufData, transitionFrameCount) => {
|
|
188
|
-
this.
|
|
189
|
-
this.animationHandler.handleTransitionData(
|
|
442
|
+
this.conversationCount++;
|
|
443
|
+
this.animationHandler.handleTransitionData(
|
|
444
|
+
protobufData,
|
|
445
|
+
transitionFrameCount
|
|
446
|
+
);
|
|
190
447
|
},
|
|
191
448
|
onTransitionEnd: (protobufData, transitionFrameCount) => {
|
|
192
|
-
this.animationHandler.handleTransitionToIdle(
|
|
449
|
+
this.animationHandler.handleTransitionToIdle(
|
|
450
|
+
protobufData,
|
|
451
|
+
transitionFrameCount
|
|
452
|
+
);
|
|
193
453
|
},
|
|
194
454
|
onSessionStart: () => {
|
|
195
455
|
},
|
|
196
456
|
onSessionEnd: () => {
|
|
197
457
|
},
|
|
198
458
|
onIdleStart: () => {
|
|
459
|
+
if (this.animationHandler.isInTransition()) {
|
|
460
|
+
this.hasActiveAnimationSession = false;
|
|
461
|
+
this.isTrackingPrimed = false;
|
|
462
|
+
logger.info(
|
|
463
|
+
"AvatarPlayer",
|
|
464
|
+
"Deferring idleStart while transition is active"
|
|
465
|
+
);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
this.hasActiveAnimationSession = false;
|
|
469
|
+
this.animationHandler.resetTracking();
|
|
470
|
+
this.isTrackingPrimed = true;
|
|
199
471
|
this.animationHandler.startIdle();
|
|
200
472
|
},
|
|
201
|
-
onStreamStats: () => {
|
|
473
|
+
onStreamStats: (stats) => {
|
|
474
|
+
if (stats.framesLost > 0 || stats.framesRecovered > 0 || stats.framesDropped > 0) {
|
|
475
|
+
logger.warn(
|
|
476
|
+
"AvatarPlayer",
|
|
477
|
+
`Stream stats: lost=${stats.framesLost}, recovered=${stats.framesRecovered}, dropped=${stats.framesDropped}, fps=${stats.framesPerSec}`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
202
480
|
}
|
|
203
481
|
};
|
|
204
482
|
await this.provider.subscribeAnimationTrack(callbacks);
|
|
@@ -208,10 +486,21 @@ class AvatarPlayer {
|
|
|
208
486
|
* @internal
|
|
209
487
|
*/
|
|
210
488
|
setupProviderEvents() {
|
|
489
|
+
this.provider.on("connected", () => {
|
|
490
|
+
this._isConnected = true;
|
|
491
|
+
});
|
|
211
492
|
this.provider.on("disconnected", () => {
|
|
212
493
|
this._isConnected = false;
|
|
494
|
+
this.hasActiveAnimationSession = false;
|
|
495
|
+
this.isTrackingPrimed = false;
|
|
213
496
|
this.animationHandler.startIdle();
|
|
214
497
|
});
|
|
498
|
+
this.provider.on("error", (error) => {
|
|
499
|
+
trackEvent("rtc_error", "error", {
|
|
500
|
+
provider: this.providerName,
|
|
501
|
+
description: error instanceof Error ? error.message : String(error)
|
|
502
|
+
});
|
|
503
|
+
});
|
|
215
504
|
}
|
|
216
505
|
}
|
|
217
506
|
export {
|