@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.
Files changed (63) hide show
  1. package/README.md +2 -410
  2. package/dist/assets/animation-worker-DOGeTjF0.js.map +1 -0
  3. package/dist/core/AvatarPlayer.d.ts +96 -12
  4. package/dist/core/AvatarPlayer.d.ts.map +1 -1
  5. package/dist/core/RTCProvider.d.ts +12 -16
  6. package/dist/core/RTCProvider.d.ts.map +1 -1
  7. package/dist/index.d.ts +3 -3
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +3 -2
  10. package/dist/index10.js +91 -39
  11. package/dist/index10.js.map +1 -1
  12. package/dist/index11.js +14 -386
  13. package/dist/index11.js.map +1 -1
  14. package/dist/index12.js +350 -64
  15. package/dist/index12.js.map +1 -1
  16. package/dist/index13.js +44 -14
  17. package/dist/index13.js.map +1 -1
  18. package/dist/index14.js +25 -44
  19. package/dist/index14.js.map +1 -1
  20. package/dist/index2.js +335 -46
  21. package/dist/index2.js.map +1 -1
  22. package/dist/index3.js +265 -54
  23. package/dist/index3.js.map +1 -1
  24. package/dist/index4.js +105 -86
  25. package/dist/index4.js.map +1 -1
  26. package/dist/index5.js +6 -2
  27. package/dist/index5.js.map +1 -1
  28. package/dist/index6.js +603 -39
  29. package/dist/index6.js.map +1 -1
  30. package/dist/index8.js +128 -167
  31. package/dist/index8.js.map +1 -1
  32. package/dist/index9.js +65 -164
  33. package/dist/index9.js.map +1 -1
  34. package/dist/providers/agora/AgoraProvider.d.ts +0 -13
  35. package/dist/providers/agora/AgoraProvider.d.ts.map +1 -1
  36. package/dist/providers/agora/index.d.ts +1 -5
  37. package/dist/providers/agora/index.d.ts.map +1 -1
  38. package/dist/providers/agora/types.d.ts.map +1 -1
  39. package/dist/providers/base/BaseProvider.d.ts +50 -8
  40. package/dist/providers/base/BaseProvider.d.ts.map +1 -1
  41. package/dist/providers/livekit/LiveKitProvider.d.ts +4 -15
  42. package/dist/providers/livekit/LiveKitProvider.d.ts.map +1 -1
  43. package/dist/providers/livekit/animation-worker.d.ts.map +1 -1
  44. package/dist/providers/livekit/index.d.ts +1 -5
  45. package/dist/providers/livekit/index.d.ts.map +1 -1
  46. package/dist/types/index.d.ts +21 -0
  47. package/dist/types/index.d.ts.map +1 -1
  48. package/dist/utils/index.d.ts +2 -0
  49. package/dist/utils/index.d.ts.map +1 -1
  50. package/dist/utils/telemetry.d.ts +22 -0
  51. package/dist/utils/telemetry.d.ts.map +1 -0
  52. package/package.json +21 -12
  53. package/dist/assets/animation-worker-CUXZycUw.js.map +0 -1
  54. package/dist/index15.js +0 -29
  55. package/dist/index15.js.map +0 -1
  56. package/dist/index16.js +0 -144
  57. package/dist/index16.js.map +0 -1
  58. package/dist/index17.js +0 -106
  59. package/dist/index17.js.map +0 -1
  60. package/dist/index18.js +0 -28
  61. package/dist/index18.js.map +0 -1
  62. package/dist/proto/animation.d.ts +0 -12
  63. 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
- function createAnimationReceiverTransform(workerOrUrl, onEvent) {
3
- const worker = workerOrUrl instanceof Worker ? workerOrUrl : new Worker(workerOrUrl, { type: "module", name: "animation-receiver" });
4
- worker.onmessage = (event) => {
5
- const { type } = event.data;
6
- if (type === "ready") {
7
- onEvent == null ? void 0 : onEvent({ type: "ready", operation: "receiver" });
8
- } else if (type === "metadata") {
9
- onEvent == null ? void 0 : onEvent(event.data);
10
- } else if (type === "animation") {
11
- onEvent == null ? void 0 : onEvent({
12
- type: "animation",
13
- protobufData: event.data.protobufData,
14
- flags: event.data.flags,
15
- isIdle: event.data.isIdle,
16
- isStart: event.data.isStart,
17
- isEnd: event.data.isEnd,
18
- frameSeq: event.data.frameSeq,
19
- isRecovered: event.data.isRecovered
20
- });
21
- } else if (type === "transition") {
22
- onEvent == null ? void 0 : onEvent({
23
- type: "transition",
24
- protobufData: event.data.protobufData,
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
- createAnimationReceiverTransform
27
+ WorkerWrapper as default
47
28
  };
48
29
  //# sourceMappingURL=index14.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index14.js","sources":["../src/providers/livekit/animation-transform.ts"],"sourcesContent":["/**\n * Animation Transform for Video Track.\n *\n * Uses RTCRtpScriptTransform to extract animation data from the video track.\n * The video track carries animation data in binary format (not actual video).\n *\n * @internal\n * @packageDocumentation\n */\n\nimport type { AnimationTransformEvent } from './types';\nimport { logger } from '../../utils';\n\nexport { METADATA_FIXED_HEADER_SIZE, PACKET_FLAGS } from './types';\n\n/**\n * Check if browser supports RTCRtpScriptTransform.\n * @returns true if RTCRtpScriptTransform is available\n * @internal\n */\nexport function supportsScriptTransform(): boolean {\n return typeof RTCRtpScriptTransform !== 'undefined';\n}\n\n/**\n * Create RTCRtpScriptTransform for animation receiver (video track).\n * @param workerOrUrl - Either a Worker instance or a URL string to the worker script\n * @param onEvent - Optional callback for transform events\n * @returns RTCRtpScriptTransform instance\n * @internal\n */\nexport function createAnimationReceiverTransform(\n workerOrUrl: Worker | string | URL,\n onEvent?: (evt: AnimationTransformEvent) => void\n): RTCRtpScriptTransform {\n // Create worker if URL is provided, otherwise use the provided Worker\n const worker =\n workerOrUrl instanceof Worker\n ? workerOrUrl\n : new Worker(workerOrUrl, { type: 'module', name: 'animation-receiver' });\n\n worker.onmessage = (event) => {\n const { type } = event.data;\n\n if (type === 'ready') {\n onEvent?.({ type: 'ready', operation: 'receiver' });\n } else if (type === 'metadata') {\n onEvent?.(event.data);\n } else if (type === 'animation') {\n onEvent?.({\n type: 'animation',\n protobufData: event.data.protobufData,\n flags: event.data.flags,\n isIdle: event.data.isIdle,\n isStart: event.data.isStart,\n isEnd: event.data.isEnd,\n frameSeq: event.data.frameSeq,\n isRecovered: event.data.isRecovered,\n });\n } else if (type === 'transition') {\n onEvent?.({\n type: 'transition',\n protobufData: event.data.protobufData,\n flags: event.data.flags,\n });\n } else if (type === 'transitionEnd') {\n onEvent?.({\n type: 'transitionEnd',\n protobufData: event.data.protobufData,\n flags: event.data.flags,\n });\n } else if (type === 'idleStart') {\n onEvent?.({ type: 'idleStart' });\n } else if (type === 'error') {\n logger.error('AnimationTransform', 'Error:', event.data.error);\n onEvent?.(event.data);\n }\n };\n\n worker.onerror = (event) => {\n logger.error('AnimationTransform', 'Worker error:', event.message);\n };\n\n return new RTCRtpScriptTransform(worker, { operation: 'receiver' });\n}\n"],"names":[],"mappings":";AA+BO,SAAS,iCACd,aACA,SACuB;AAEvB,QAAM,SACJ,uBAAuB,SACnB,cACA,IAAI,OAAO,aAAa,EAAE,MAAM,UAAU,MAAM,qBAAA,CAAsB;AAE5E,SAAO,YAAY,CAAC,UAAU;AAC5B,UAAM,EAAE,SAAS,MAAM;AAEvB,QAAI,SAAS,SAAS;AACpB,yCAAU,EAAE,MAAM,SAAS,WAAW;IACxC,WAAW,SAAS,YAAY;AAC9B,yCAAU,MAAM;AAAA,IAClB,WAAW,SAAS,aAAa;AAC/B,yCAAU;AAAA,QACR,MAAM;AAAA,QACN,cAAc,MAAM,KAAK;AAAA,QACzB,OAAO,MAAM,KAAK;AAAA,QAClB,QAAQ,MAAM,KAAK;AAAA,QACnB,SAAS,MAAM,KAAK;AAAA,QACpB,OAAO,MAAM,KAAK;AAAA,QAClB,UAAU,MAAM,KAAK;AAAA,QACrB,aAAa,MAAM,KAAK;AAAA,MAAA;AAAA,IAE5B,WAAW,SAAS,cAAc;AAChC,yCAAU;AAAA,QACR,MAAM;AAAA,QACN,cAAc,MAAM,KAAK;AAAA,QACzB,OAAO,MAAM,KAAK;AAAA,MAAA;AAAA,IAEtB,WAAW,SAAS,iBAAiB;AACnC,yCAAU;AAAA,QACR,MAAM;AAAA,QACN,cAAc,MAAM,KAAK;AAAA,QACzB,OAAO,MAAM,KAAK;AAAA,MAAA;AAAA,IAEtB,WAAW,SAAS,aAAa;AAC/B,yCAAU,EAAE,MAAM;IACpB,WAAW,SAAS,SAAS;AAC3B,aAAO,MAAM,sBAAsB,UAAU,MAAM,KAAK,KAAK;AAC7D,yCAAU,MAAM;AAAA,IAClB;AAAA,EACF;AAEA,SAAO,UAAU,CAAC,UAAU;AAC1B,WAAO,MAAM,sBAAsB,iBAAiB,MAAM,OAAO;AAAA,EACnE;AAEA,SAAO,IAAI,sBAAsB,QAAQ,EAAE,WAAW,YAAY;AACpE;"}
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, "mediaStream", null);
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
- await this.setupAnimationCallbacks();
51
- await this.provider.connect(config);
52
- this._isConnected = true;
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.localAudioTrack) {
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
- * Start publishing microphone audio to the RTC server.
71
- * Requests microphone permission and starts publishing.
72
- * @throws Error if permission denied or no microphone found
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 startPublishing() {
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
- return;
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 microphone audio.
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 stopPublishing() {
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
- if (this.mediaStream) {
93
- this.mediaStream.getTracks().forEach((track) => track.stop());
94
- this.mediaStream = null;
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
- this.provider.on(event, handler);
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
- this.provider.off(event, handler);
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
- renderFrame(flame, startIdle) {
391
+ renderFromProtobuf(protobufData) {
392
+ avatarView.renderFromProtobuf(protobufData);
393
+ },
394
+ renderFrame(frame, startIdle) {
395
+ const view = avatarView;
151
396
  if (startIdle) {
152
- avatarView.renderFlame(void 0, true);
153
- } else if (flame) {
154
- avatarView.renderFlame(flame);
397
+ view.renderFlame(void 0, true);
398
+ } else if (frame) {
399
+ view.renderFlame(frame);
155
400
  }
156
401
  },
157
- async generateTransitionFromIdle(targetFrame, frameCount) {
158
- return avatarView.generateTransition({
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 (metadata.isStart) {
178
- this.animationHandler.resetTracking();
179
- this.provider.playRemoteAudio();
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.provider.pauseRemoteAudio();
189
- this.animationHandler.handleTransitionData(protobufData, transitionFrameCount);
442
+ this.conversationCount++;
443
+ this.animationHandler.handleTransitionData(
444
+ protobufData,
445
+ transitionFrameCount
446
+ );
190
447
  },
191
448
  onTransitionEnd: (protobufData, transitionFrameCount) => {
192
- this.animationHandler.handleTransitionToIdle(protobufData, transitionFrameCount);
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 {