@spatialwalk/avatarkit-rtc 1.0.0-beta.8 → 1.0.0-beta.9
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 -571
- package/dist/assets/animation-worker-DOGeTjF0.js.map +1 -0
- package/dist/core/AvatarPlayer.d.ts +8 -3
- package/dist/core/AvatarPlayer.d.ts.map +1 -1
- package/dist/core/RTCProvider.d.ts +12 -5
- 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 +86 -47
- package/dist/index10.js.map +1 -1
- package/dist/index11.js +14 -104
- package/dist/index11.js.map +1 -1
- package/dist/index12.js +390 -14
- package/dist/index12.js.map +1 -1
- package/dist/index13.js +137 -349
- package/dist/index13.js.map +1 -1
- package/dist/index14.js.map +1 -1
- package/dist/index15.js +1 -1
- package/dist/index2.js +38 -17
- package/dist/index2.js.map +1 -1
- package/dist/index3.js +141 -42
- package/dist/index3.js.map +1 -1
- package/dist/index4.js +101 -70
- package/dist/index4.js.map +1 -1
- package/dist/index5.js +6 -2
- package/dist/index5.js.map +1 -1
- package/dist/index6.js +73 -18
- package/dist/index6.js.map +1 -1
- package/dist/index8.js +5 -2
- 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.map +1 -1
- package/dist/providers/agora/types.d.ts.map +1 -1
- package/dist/providers/base/BaseProvider.d.ts +9 -13
- package/dist/providers/base/BaseProvider.d.ts.map +1 -1
- package/dist/providers/livekit/LiveKitProvider.d.ts +4 -2
- package/dist/providers/livekit/LiveKitProvider.d.ts.map +1 -1
- package/dist/providers/livekit/animation-worker.d.ts.map +1 -1
- package/dist/types/index.d.ts +21 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +12 -3
- package/dist/assets/animation-worker-CdhDm7lL.js.map +0 -1
package/dist/index15.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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(METADATA_FIXED_HEADER_SIZE, METADATA_FIXED_HEADER_SIZE + msgLen);\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(decompressed.buffer, decompressed.byteOffset, decompressed.byteLength);\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 type: "animation",\n flags,\n isIdle,\n isStart,\n isEnd,\n isRecovered,\n frameSeq,\n protobufData: protobufBuffer\n }, { transfer: [protobufBuffer] });\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({ data: parsed2.prev2Data, seq: currentSeq - 2 });\n }\n if (missedFrames >= 1 && parsed2.prev1Data) {\n framesToRecover.push({ data: parsed2.prev1Data, seq: currentSeq - 1 });\n }\n const recovered = framesToRecover.length;\n if (recovered > 0) {\n framesRecovered += recovered;\n for (const frame2 of framesToRecover) {\n sendAnimationToMainThread(frame2.data, meta.flags & ~PACKET_FLAG_REDUNDANT, frame2.seq, true);\n }\n }\n sendAnimationToMainThread(parsed2.currentData, meta.flags & ~PACKET_FLAG_REDUNDANT, currentSeq, false);\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 type: "transition",\n flags: meta.flags,\n protobufData: protobufBuffer\n }, { transfer: [protobufBuffer] });\n }).catch((err) => {\n console.error(`[Animation Worker] Gzip decompress error (transition):`, err);\n });\n } else {\n const protobufBuffer = new ArrayBuffer(meta.protobufData.byteLength);\n new Uint8Array(protobufBuffer).set(meta.protobufData);\n self.postMessage({\n type: "transition",\n flags: meta.flags,\n protobufData: protobufBuffer\n }, { transfer: [protobufBuffer] });\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 type: "transitionEnd",\n flags: meta.flags,\n protobufData: protobufBuffer\n }, { transfer: [protobufBuffer] });\n }).catch((err) => {\n console.error(`[Animation Worker] Gzip decompress error (transitionEnd):`, err);\n });\n } else {\n const protobufBuffer = new ArrayBuffer(meta.protobufData.byteLength);\n new Uint8Array(protobufBuffer).set(meta.protobufData);\n self.postMessage({\n type: "transitionEnd",\n flags: meta.flags,\n protobufData: protobufBuffer\n }, { transfer: [protobufBuffer] });\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(parsed2.currentData, meta.flags & ~PACKET_FLAG_REDUNDANT, parsed2.frameSeq, false);\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 type: "animation",\n flags: meta.flags,\n isIdle: false,\n isStart,\n isEnd,\n frameSeq: -1,\n // Unknown sequence\n protobufData: protobufBuffer\n }, { transfer: [protobufBuffer] });\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 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({ type: "error", error: `Animation receiver pipe error: ${err}` });\n });\n self.postMessage({ type: "ready", operation: "receiver" });\n }\n } catch (err) {\n console.error("[Animation Worker] Setup error:", err);\n self.postMessage({ type: "error", error: `Animation transform setup error: ${err}` });\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-CdhDm7lL.js.map\n';
|
|
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
2
|
const blob = typeof self !== "undefined" && self.Blob && new Blob(["URL.revokeObjectURL(import.meta.url);", jsContent], { type: "text/javascript;charset=utf-8" });
|
|
3
3
|
function WorkerWrapper(options) {
|
|
4
4
|
let objURL;
|
package/dist/index2.js
CHANGED
|
@@ -70,6 +70,14 @@ class AvatarPlayer {
|
|
|
70
70
|
await this.provider.connect(config);
|
|
71
71
|
this._isConnected = true;
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Pre-warm a future RTC connection when the provider supports it.
|
|
75
|
+
* This is best-effort and does not change the connected state.
|
|
76
|
+
*/
|
|
77
|
+
async prepareConnection(config) {
|
|
78
|
+
var _a, _b;
|
|
79
|
+
await ((_b = (_a = this.provider).prepareConnection) == null ? void 0 : _b.call(_a, config));
|
|
80
|
+
}
|
|
73
81
|
/**
|
|
74
82
|
* Disconnect from RTC server.
|
|
75
83
|
* Stops all tracks and cleans up resources.
|
|
@@ -89,22 +97,22 @@ class AvatarPlayer {
|
|
|
89
97
|
}
|
|
90
98
|
/**
|
|
91
99
|
* Publish an audio track to the RTC server.
|
|
92
|
-
*
|
|
100
|
+
*
|
|
93
101
|
* The audio source is controlled externally - you can pass any audio track:
|
|
94
102
|
* - Microphone: `getUserMedia({ audio: true })`
|
|
95
103
|
* - Browser audio: `audioElement.captureStream()`
|
|
96
104
|
* - Web Audio API: `audioContext.createMediaStreamDestination().stream`
|
|
97
105
|
* - Screen share audio: `getDisplayMedia({ audio: true })`
|
|
98
|
-
*
|
|
106
|
+
*
|
|
99
107
|
* @param track - MediaStreamTrack to publish (must be an audio track)
|
|
100
108
|
* @throws Error if not connected or track is invalid
|
|
101
|
-
*
|
|
109
|
+
*
|
|
102
110
|
* @example
|
|
103
111
|
* ```typescript
|
|
104
112
|
* // Microphone
|
|
105
113
|
* const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
106
114
|
* await player.publishAudio(stream.getAudioTracks()[0]);
|
|
107
|
-
*
|
|
115
|
+
*
|
|
108
116
|
* // Browser audio element
|
|
109
117
|
* const audioEl = document.querySelector('audio');
|
|
110
118
|
* const stream = audioEl.captureStream();
|
|
@@ -137,12 +145,12 @@ class AvatarPlayer {
|
|
|
137
145
|
}
|
|
138
146
|
/**
|
|
139
147
|
* Start publishing microphone audio.
|
|
140
|
-
*
|
|
148
|
+
*
|
|
141
149
|
* This requests microphone permission and publishes it to the RTC session.
|
|
142
150
|
* For custom audio sources (e.g., audio files), use `publishAudio(track)` instead.
|
|
143
|
-
*
|
|
151
|
+
*
|
|
144
152
|
* @throws Error if not connected, permission denied, or no microphone found
|
|
145
|
-
*
|
|
153
|
+
*
|
|
146
154
|
* @example
|
|
147
155
|
* ```typescript
|
|
148
156
|
* await player.startPublishing();
|
|
@@ -157,13 +165,15 @@ class AvatarPlayer {
|
|
|
157
165
|
if (this.microphoneStream) {
|
|
158
166
|
return;
|
|
159
167
|
}
|
|
160
|
-
this.microphoneStream = await navigator.mediaDevices.getUserMedia({
|
|
168
|
+
this.microphoneStream = await navigator.mediaDevices.getUserMedia({
|
|
169
|
+
audio: true
|
|
170
|
+
});
|
|
161
171
|
const track = this.microphoneStream.getAudioTracks()[0];
|
|
162
172
|
await this.publishAudio(track);
|
|
163
173
|
}
|
|
164
174
|
/**
|
|
165
175
|
* Stop publishing microphone audio and release the microphone.
|
|
166
|
-
*
|
|
176
|
+
*
|
|
167
177
|
* This stops the microphone track and releases the device.
|
|
168
178
|
* If you used `publishAudio()` with a custom track, use `unpublishAudio()` instead.
|
|
169
179
|
*/
|
|
@@ -184,19 +194,19 @@ class AvatarPlayer {
|
|
|
184
194
|
}
|
|
185
195
|
/**
|
|
186
196
|
* Get the native RTC client object.
|
|
187
|
-
*
|
|
197
|
+
*
|
|
188
198
|
* Returns the underlying RTC client for advanced use cases:
|
|
189
199
|
* - LiveKit: Returns the Room instance
|
|
190
200
|
* - Agora: Returns the IAgoraRTCClient instance
|
|
191
|
-
*
|
|
201
|
+
*
|
|
192
202
|
* @returns The native RTC client object, or null if not connected
|
|
193
|
-
*
|
|
203
|
+
*
|
|
194
204
|
* @example
|
|
195
205
|
* ```typescript
|
|
196
206
|
* // Access LiveKit Room
|
|
197
207
|
* const room = player.getNativeClient() as Room;
|
|
198
208
|
* console.log('Participants:', room?.remoteParticipants.size);
|
|
199
|
-
*
|
|
209
|
+
*
|
|
200
210
|
* // Access Agora Client
|
|
201
211
|
* const client = player.getNativeClient() as IAgoraRTCClient;
|
|
202
212
|
* console.log('State:', client?.connectionState);
|
|
@@ -269,7 +279,9 @@ class AvatarPlayer {
|
|
|
269
279
|
return;
|
|
270
280
|
}
|
|
271
281
|
if (!this.lastConnectionConfig) {
|
|
272
|
-
throw new Error(
|
|
282
|
+
throw new Error(
|
|
283
|
+
"Cannot reconnect: no previous connection. Call connect() first."
|
|
284
|
+
);
|
|
273
285
|
}
|
|
274
286
|
this.isReconnecting = true;
|
|
275
287
|
logger.info("AvatarPlayer", "Attempting reconnection...");
|
|
@@ -346,10 +358,16 @@ class AvatarPlayer {
|
|
|
346
358
|
);
|
|
347
359
|
},
|
|
348
360
|
onTransition: (protobufData, transitionFrameCount) => {
|
|
349
|
-
this.animationHandler.handleTransitionData(
|
|
361
|
+
this.animationHandler.handleTransitionData(
|
|
362
|
+
protobufData,
|
|
363
|
+
transitionFrameCount
|
|
364
|
+
);
|
|
350
365
|
},
|
|
351
366
|
onTransitionEnd: (protobufData, transitionFrameCount) => {
|
|
352
|
-
this.animationHandler.handleTransitionToIdle(
|
|
367
|
+
this.animationHandler.handleTransitionToIdle(
|
|
368
|
+
protobufData,
|
|
369
|
+
transitionFrameCount
|
|
370
|
+
);
|
|
353
371
|
},
|
|
354
372
|
onSessionStart: () => {
|
|
355
373
|
},
|
|
@@ -359,7 +377,10 @@ class AvatarPlayer {
|
|
|
359
377
|
if (this.animationHandler.isInTransition()) {
|
|
360
378
|
this.hasActiveAnimationSession = false;
|
|
361
379
|
this.isTrackingPrimed = false;
|
|
362
|
-
logger.info(
|
|
380
|
+
logger.info(
|
|
381
|
+
"AvatarPlayer",
|
|
382
|
+
"Deferring idleStart while transition is active"
|
|
383
|
+
);
|
|
363
384
|
return;
|
|
364
385
|
}
|
|
365
386
|
this.hasActiveAnimationSession = false;
|
package/dist/index2.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index2.js","sources":["../src/core/AvatarPlayer.ts"],"sourcesContent":["/**\n * AvatarPlayer - Unified entry point for RTC avatar streaming.\n *\n * This class provides a unified API that shields applications from\n * differences between different RTC providers (LiveKit, Agora, etc.)\n * and integrates seamlessly with @spatialwalk/avatarkit for rendering.\n *\n * @packageDocumentation\n */\n\nimport type { RTCProvider } from './RTCProvider';\nimport type { RTCConnectionConfig } from '../types';\nimport { AnimationHandler } from './AnimationHandler';\nimport type { AnimationTrackCallbacks, RTCProviderEvents } from './types';\nimport { configureLogger, logger, type LogLevel } from '../utils';\n\n// Import AvatarView type from avatarkit\nimport type { AvatarView, KeyframeData } from '@spatialwalk/avatarkit';\n\n/**\n * Options for configuring AvatarPlayer.\n */\nexport interface AvatarPlayerOptions {\n /**\n * Log level for SDK output.\n * - 'info': Show all logs (debug mode)\n * - 'warning': Show warnings and errors (default)\n * - 'error': Show only errors\n * - 'none': Disable all logs\n */\n logLevel?: LogLevel;\n\n /**\n * Enable jitter buffer for smoother animation playback.\n * When enabled, frames are buffered and rendered in sequence order at a steady 25fps,\n * absorbing network jitter and out-of-order delivery.\n * Default: true\n */\n enableJitterBuffer?: boolean;\n\n /**\n * Maximum delay (in ms) a frame can sit in the jitter buffer before being rendered.\n * Only used when enableJitterBuffer is true.\n * Default: 80 (2 frames at 25fps)\n */\n maxBufferDelayMs?: number;\n}\n\n/**\n * Unified Avatar Player.\n *\n * Main entry point for applications to interact with RTC avatar streaming.\n * Handles connection management, audio publishing, animation rendering,\n * and coordinates with avatarkit for display.\n *\n * @example\n * ```typescript\n * import { AvatarPlayer, LiveKitProvider } from '@spatialwalk/avatarkit-rtc';\n * import { AvatarView, Avatar } from '@spatialwalk/avatarkit';\n *\n * // Create avatar and view (from avatarkit)\n * const avatar = await Avatar.create(characterId);\n * const avatarView = new AvatarView(avatar, container);\n *\n * // Create player\n * const provider = new LiveKitProvider();\n * const player = new AvatarPlayer(provider, avatarView);\n *\n * // Connect\n * await player.connect({ url: 'wss://...', token: '...' });\n *\n * // Option 1: Use microphone (most common)\n * await player.startPublishing();\n * await player.stopPublishing();\n *\n * // Option 2: Use custom audio source\n * const stream = await navigator.mediaDevices.getUserMedia({ audio: true });\n * await player.publishAudio(stream.getAudioTracks()[0]);\n * await player.unpublishAudio();\n * stream.getTracks().forEach(t => t.stop());\n *\n * // Disconnect\n * await player.disconnect();\n * ```\n */\nexport class AvatarPlayer {\n /** @internal */\n private provider: RTCProvider;\n\n /** @internal */\n private avatarView: AvatarView;\n\n /** @internal */\n private animationHandler: AnimationHandler;\n\n /** @internal */\n private _isConnected = false;\n\n /** @internal */\n private localAudioTrack: MediaStreamTrack | null = null;\n\n /** @internal */\n private microphoneStream: MediaStream | null = null;\n\n /** @internal */\n private eventHandlers: Map<string, Set<Function>> = new Map();\n\n /** @internal */\n private lastConnectionConfig: RTCConnectionConfig | null = null;\n\n /** @internal */\n private isReconnecting = false;\n\n /** @internal */\n private hasActiveAnimationSession = false;\n\n /** @internal */\n private isTrackingPrimed = false;\n\n /**\n * Create a new AvatarPlayer instance.\n * @param provider - RTC provider implementation (e.g., LiveKitProvider)\n * @param avatarView - AvatarView instance from @spatialwalk/avatarkit\n * @param options - Optional configuration for transitions\n */\n constructor(\n provider: RTCProvider,\n avatarView: AvatarView,\n options?: AvatarPlayerOptions\n ) {\n this.provider = provider;\n this.avatarView = avatarView;\n\n // Configure logging if specified\n if (options?.logLevel !== undefined) {\n configureLogger({ level: options.logLevel });\n }\n\n // Create internal renderer adapter\n const renderer = this.createRendererAdapter();\n\n this.animationHandler = new AnimationHandler(renderer, {\n enableJitterBuffer: options?.enableJitterBuffer,\n maxBufferDelayMs: options?.maxBufferDelayMs,\n onStreamStalled: () => {\n // AnimationHandler already switches to idle state\n // Just notify external listeners, let them decide what to do (e.g., call reconnect())\n this.hasActiveAnimationSession = false;\n this.isTrackingPrimed = false;\n this.emit('stalled');\n },\n });\n\n // Setup provider event handlers\n this.setupProviderEvents();\n }\n\n /**\n * Check if currently connected to RTC server.\n */\n get isConnected(): boolean {\n return this._isConnected;\n }\n\n /**\n * Connect to RTC server.\n * @param config - Connection configuration (URL, token, room name, etc.)\n * @throws Error if already connected or connection fails\n */\n async connect(config: RTCConnectionConfig): Promise<void> {\n if (this._isConnected) {\n throw new Error('Already connected. Please disconnect first.');\n }\n\n // Save config for potential reconnection\n this.lastConnectionConfig = config;\n\n // Setup animation callbacks before connecting\n await this.setupAnimationCallbacks();\n await this.provider.connect(config);\n this._isConnected = true;\n }\n\n /**\n * Disconnect from RTC server.\n * Stops all tracks and cleans up resources.\n */\n async disconnect(): Promise<void> {\n if (!this._isConnected) {\n return;\n }\n\n // Stop microphone if active\n if (this.microphoneStream) {\n await this.stopPublishing();\n } else if (this.localAudioTrack) {\n // Stop custom audio track\n await this.unpublishAudio();\n }\n\n await this.provider.disconnect();\n this.animationHandler.dispose();\n this._isConnected = false;\n }\n\n /**\n * Publish an audio track to the RTC server.\n * \n * The audio source is controlled externally - you can pass any audio track:\n * - Microphone: `getUserMedia({ audio: true })`\n * - Browser audio: `audioElement.captureStream()`\n * - Web Audio API: `audioContext.createMediaStreamDestination().stream`\n * - Screen share audio: `getDisplayMedia({ audio: true })`\n * \n * @param track - MediaStreamTrack to publish (must be an audio track)\n * @throws Error if not connected or track is invalid\n * \n * @example\n * ```typescript\n * // Microphone\n * const stream = await navigator.mediaDevices.getUserMedia({ audio: true });\n * await player.publishAudio(stream.getAudioTracks()[0]);\n * \n * // Browser audio element\n * const audioEl = document.querySelector('audio');\n * const stream = audioEl.captureStream();\n * await player.publishAudio(stream.getAudioTracks()[0]);\n * ```\n */\n async publishAudio(track: MediaStreamTrack): Promise<void> {\n if (!this._isConnected) {\n throw new Error('Not connected. Please call connect() first.');\n }\n\n if (track.kind !== 'audio') {\n throw new Error('Invalid track: expected audio track');\n }\n\n // Unpublish existing track if any\n if (this.localAudioTrack) {\n await this.unpublishAudio();\n }\n\n this.localAudioTrack = track;\n await this.provider.publishAudioTrack(track);\n }\n\n /**\n * Stop publishing audio.\n * Note: This does NOT stop the track - the caller is responsible for managing the track lifecycle.\n */\n async unpublishAudio(): Promise<void> {\n if (!this.localAudioTrack) {\n return;\n }\n\n await this.provider.unpublishAudioTrack();\n this.localAudioTrack = null;\n }\n\n /**\n * Start publishing microphone audio.\n * \n * This requests microphone permission and publishes it to the RTC session.\n * For custom audio sources (e.g., audio files), use `publishAudio(track)` instead.\n * \n * @throws Error if not connected, permission denied, or no microphone found\n * \n * @example\n * ```typescript\n * await player.startPublishing();\n * // ... later\n * await player.stopPublishing();\n * ```\n */\n async startPublishing(): Promise<void> {\n if (!this._isConnected) {\n throw new Error('Not connected. Please call connect() first.');\n }\n\n if (this.microphoneStream) {\n return; // Already using microphone\n }\n\n // Request microphone permission\n this.microphoneStream = await navigator.mediaDevices.getUserMedia({ audio: true });\n const track = this.microphoneStream.getAudioTracks()[0];\n \n await this.publishAudio(track);\n }\n\n /**\n * Stop publishing microphone audio and release the microphone.\n * \n * This stops the microphone track and releases the device.\n * If you used `publishAudio()` with a custom track, use `unpublishAudio()` instead.\n */\n async stopPublishing(): Promise<void> {\n if (!this.microphoneStream) {\n return;\n }\n\n await this.unpublishAudio();\n\n // Stop and release microphone\n this.microphoneStream.getTracks().forEach((track) => track.stop());\n this.microphoneStream = null;\n }\n\n /**\n * Get current connection state.\n * @returns Connection state string (e.g., 'connected', 'disconnected')\n */\n getConnectionState(): string {\n return this.provider.getConnectionState();\n }\n\n /**\n * Get the native RTC client object.\n * \n * Returns the underlying RTC client for advanced use cases:\n * - LiveKit: Returns the Room instance\n * - Agora: Returns the IAgoraRTCClient instance\n * \n * @returns The native RTC client object, or null if not connected\n * \n * @example\n * ```typescript\n * // Access LiveKit Room\n * const room = player.getNativeClient() as Room;\n * console.log('Participants:', room?.remoteParticipants.size);\n * \n * // Access Agora Client\n * const client = player.getNativeClient() as IAgoraRTCClient;\n * console.log('State:', client?.connectionState);\n * ```\n */\n getNativeClient(): unknown {\n return this.provider.getNativeClient();\n }\n\n /**\n * Add event listener.\n * @param event - Event name ('connected', 'disconnected', 'error', 'stalled', etc.)\n * @param handler - Event handler function\n */\n on(event: string, handler: Function): void {\n if (event === 'stalled') {\n // Handle stalled event locally\n if (!this.eventHandlers.has(event)) {\n this.eventHandlers.set(event, new Set());\n }\n this.eventHandlers.get(event)!.add(handler);\n } else {\n // Forward other events to provider\n this.provider.on(event as any, handler as any);\n }\n }\n\n /**\n * Remove event listener.\n * @param event - Event name\n * @param handler - Event handler function (must be same reference as added)\n */\n off(event: string, handler: Function): void {\n if (event === 'stalled') {\n this.eventHandlers.get(event)?.delete(handler);\n } else {\n this.provider.off(event as any, handler as any);\n }\n }\n\n /**\n * Emit an event to local listeners.\n * @internal\n */\n private emit(event: string, ...args: unknown[]): void {\n const handlers = this.eventHandlers.get(event);\n if (handlers) {\n handlers.forEach((handler) => {\n try {\n handler(...args);\n } catch (e) {\n logger.error('AvatarPlayer', `Error in ${event} handler:`, e);\n }\n });\n }\n }\n\n /**\n * Reconnect to RTC server using the last connection configuration.\n * Useful for recovering from connection issues or stream stalls.\n *\n * @example\n * ```typescript\n * player.on('stalled', async () => {\n * console.log('Stream stalled, attempting reconnection...');\n * await player.reconnect();\n * });\n * ```\n *\n * @throws Error if no previous connection config exists or reconnection fails\n */\n async reconnect(): Promise<void> {\n if (this.isReconnecting) {\n logger.warn('AvatarPlayer', 'Already attempting reconnection, skipping');\n return;\n }\n\n if (!this.lastConnectionConfig) {\n throw new Error('Cannot reconnect: no previous connection. Call connect() first.');\n }\n\n this.isReconnecting = true;\n logger.info('AvatarPlayer', 'Attempting reconnection...');\n\n try {\n // Disconnect first if connected\n if (this._isConnected) {\n await this.disconnect();\n }\n\n // Wait a short moment before reconnecting\n await new Promise((resolve) => setTimeout(resolve, 500));\n\n // Reconnect\n await this.connect(this.lastConnectionConfig);\n logger.info('AvatarPlayer', 'Reconnection successful');\n } catch (error) {\n logger.error('AvatarPlayer', 'Reconnection failed:', error);\n throw error;\n } finally {\n this.isReconnecting = false;\n }\n }\n\n /**\n * Create internal renderer adapter that bridges AnimationHandler to AvatarView.\n * @internal\n */\n private createRendererAdapter() {\n const avatarView = this.avatarView;\n\n return {\n renderFrame(flame: KeyframeData | undefined, startIdle?: boolean): void {\n if (startIdle) {\n // Enable idle rendering mode\n avatarView.renderFlame(undefined as unknown as KeyframeData, true);\n } else if (flame) {\n // Render the animation frame\n avatarView.renderFlame(flame);\n }\n },\n\n async generateTransitionFromIdle(\n targetFrame: KeyframeData,\n frameCount: number\n ): Promise<KeyframeData[]> {\n // Use avatarkit's generateTransition with from=undefined (current idle frame)\n return avatarView.generateTransition({\n to: targetFrame,\n frameCount,\n useLinear: true, // Linear interpolation for idle->speaking\n });\n },\n\n isReady(): boolean {\n return true; // AvatarView is always ready once created\n },\n };\n }\n\n /**\n * Setup animation track callbacks.\n * @internal\n */\n private async setupAnimationCallbacks(): Promise<void> {\n const callbacks: AnimationTrackCallbacks = {\n onAnimationData: (protobufData, metadata) => {\n // Reset tracking only once per session to avoid duplicate start-packet resets.\n if (!this.hasActiveAnimationSession) {\n if (!this.isTrackingPrimed) {\n this.animationHandler.resetTracking();\n }\n\n this.hasActiveAnimationSession = true;\n this.isTrackingPrimed = false;\n\n if (!metadata.isStart) {\n logger.info(\n 'AvatarPlayer',\n `Session start inferred from frame seq=${metadata.frameSeq ?? 'n/a'}`\n );\n }\n } else if (metadata.isStart) {\n logger.info(\n 'AvatarPlayer',\n `Ignoring duplicate session start frame seq=${metadata.frameSeq ?? 'n/a'}`\n );\n }\n\n this.animationHandler.handleAnimationData(\n protobufData,\n metadata.frameSeq,\n metadata.isRecovered\n );\n },\n onTransition: (protobufData, transitionFrameCount) => {\n this.animationHandler.handleTransitionData(protobufData, transitionFrameCount);\n },\n onTransitionEnd: (protobufData, transitionFrameCount) => {\n this.animationHandler.handleTransitionToIdle(protobufData, transitionFrameCount);\n },\n onSessionStart: () => {\n // Session started - can be used for UI feedback\n },\n onSessionEnd: () => {\n // Session ended - can be used for UI feedback\n },\n onIdleStart: () => {\n // Idle packets can race with start-transition packets; avoid rendering idle mid-transition.\n if (this.animationHandler.isInTransition()) {\n // Keep session bookkeeping in sync even if visual idle is deferred.\n this.hasActiveAnimationSession = false;\n this.isTrackingPrimed = false;\n logger.info('AvatarPlayer', 'Deferring idleStart while transition is active');\n return;\n }\n\n this.hasActiveAnimationSession = false;\n this.animationHandler.resetTracking();\n this.isTrackingPrimed = true;\n this.animationHandler.startIdle();\n },\n onStreamStats: (stats) => {\n // Log packet loss/recovery stats\n if (stats.framesLost > 0 || stats.framesRecovered > 0 || stats.framesDropped > 0) {\n logger.warn(\n 'AvatarPlayer',\n `Stream stats: lost=${stats.framesLost}, recovered=${stats.framesRecovered}, dropped=${stats.framesDropped}, fps=${stats.framesPerSec}`\n );\n }\n },\n };\n\n await this.provider.subscribeAnimationTrack(callbacks);\n }\n\n /**\n * Setup provider event handlers.\n * @internal\n */\n private setupProviderEvents(): void {\n this.provider.on('connected', () => {\n this._isConnected = true;\n });\n\n this.provider.on('disconnected', () => {\n this._isConnected = false;\n this.hasActiveAnimationSession = false;\n this.isTrackingPrimed = false;\n // Return to idle animation when disconnected\n this.animationHandler.startIdle();\n });\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index2.js","sources":["../src/core/AvatarPlayer.ts"],"sourcesContent":["/**\n * AvatarPlayer - Unified entry point for RTC avatar streaming.\n *\n * This class provides a unified API that shields applications from\n * differences between different RTC providers (LiveKit, Agora, etc.)\n * and integrates seamlessly with @spatialwalk/avatarkit for rendering.\n *\n * @packageDocumentation\n */\n\nimport type { RTCProvider } from './RTCProvider';\nimport type { RTCConnectionConfig, RTCPrepareConnectionConfig } from '../types';\nimport { AnimationHandler } from './AnimationHandler';\nimport type { AnimationTrackCallbacks } from './types';\nimport { configureLogger, logger, type LogLevel } from '../utils';\n\n// Import AvatarView type from avatarkit\nimport type { AvatarView, KeyframeData } from '@spatialwalk/avatarkit';\n\n/**\n * Options for configuring AvatarPlayer.\n */\nexport interface AvatarPlayerOptions {\n /**\n * Log level for SDK output.\n * - 'info': Show all logs (debug mode)\n * - 'warning': Show warnings and errors (default)\n * - 'error': Show only errors\n * - 'none': Disable all logs\n */\n logLevel?: LogLevel;\n\n /**\n * Enable jitter buffer for smoother animation playback.\n * When enabled, frames are buffered and rendered in sequence order at a steady 25fps,\n * absorbing network jitter and out-of-order delivery.\n * Default: true\n */\n enableJitterBuffer?: boolean;\n\n /**\n * Maximum delay (in ms) a frame can sit in the jitter buffer before being rendered.\n * Only used when enableJitterBuffer is true.\n * Default: 80 (2 frames at 25fps)\n */\n maxBufferDelayMs?: number;\n}\n\n/**\n * Unified Avatar Player.\n *\n * Main entry point for applications to interact with RTC avatar streaming.\n * Handles connection management, audio publishing, animation rendering,\n * and coordinates with avatarkit for display.\n *\n * @example\n * ```typescript\n * import { AvatarPlayer, LiveKitProvider } from '@spatialwalk/avatarkit-rtc';\n * import { AvatarView, Avatar } from '@spatialwalk/avatarkit';\n *\n * // Create avatar and view (from avatarkit)\n * const avatar = await Avatar.create(characterId);\n * const avatarView = new AvatarView(avatar, container);\n *\n * // Create player\n * const provider = new LiveKitProvider();\n * const player = new AvatarPlayer(provider, avatarView);\n *\n * // Connect\n * await player.connect({ url: 'wss://...', token: '...' });\n *\n * // Option 1: Use microphone (most common)\n * await player.startPublishing();\n * await player.stopPublishing();\n *\n * // Option 2: Use custom audio source\n * const stream = await navigator.mediaDevices.getUserMedia({ audio: true });\n * await player.publishAudio(stream.getAudioTracks()[0]);\n * await player.unpublishAudio();\n * stream.getTracks().forEach(t => t.stop());\n *\n * // Disconnect\n * await player.disconnect();\n * ```\n */\nexport class AvatarPlayer {\n /** @internal */\n private provider: RTCProvider;\n\n /** @internal */\n private avatarView: AvatarView;\n\n /** @internal */\n private animationHandler: AnimationHandler;\n\n /** @internal */\n private _isConnected = false;\n\n /** @internal */\n private localAudioTrack: MediaStreamTrack | null = null;\n\n /** @internal */\n private microphoneStream: MediaStream | null = null;\n\n /** @internal */\n private eventHandlers: Map<string, Set<(...args: unknown[]) => void>> =\n new Map();\n\n /** @internal */\n private lastConnectionConfig: RTCConnectionConfig | null = null;\n\n /** @internal */\n private isReconnecting = false;\n\n /** @internal */\n private hasActiveAnimationSession = false;\n\n /** @internal */\n private isTrackingPrimed = false;\n\n /**\n * Create a new AvatarPlayer instance.\n * @param provider - RTC provider implementation (e.g., LiveKitProvider)\n * @param avatarView - AvatarView instance from @spatialwalk/avatarkit\n * @param options - Optional configuration for transitions\n */\n constructor(\n provider: RTCProvider,\n avatarView: AvatarView,\n options?: AvatarPlayerOptions,\n ) {\n this.provider = provider;\n this.avatarView = avatarView;\n\n // Configure logging if specified\n if (options?.logLevel !== undefined) {\n configureLogger({ level: options.logLevel });\n }\n\n // Create internal renderer adapter\n const renderer = this.createRendererAdapter();\n\n this.animationHandler = new AnimationHandler(renderer, {\n enableJitterBuffer: options?.enableJitterBuffer,\n maxBufferDelayMs: options?.maxBufferDelayMs,\n onStreamStalled: () => {\n // AnimationHandler already switches to idle state\n // Just notify external listeners, let them decide what to do (e.g., call reconnect())\n this.hasActiveAnimationSession = false;\n this.isTrackingPrimed = false;\n this.emit('stalled');\n },\n });\n\n // Setup provider event handlers\n this.setupProviderEvents();\n }\n\n /**\n * Check if currently connected to RTC server.\n */\n get isConnected(): boolean {\n return this._isConnected;\n }\n\n /**\n * Connect to RTC server.\n * @param config - Connection configuration (URL, token, room name, etc.)\n * @throws Error if already connected or connection fails\n */\n async connect(config: RTCConnectionConfig): Promise<void> {\n if (this._isConnected) {\n throw new Error('Already connected. Please disconnect first.');\n }\n\n // Save config for potential reconnection\n this.lastConnectionConfig = config;\n\n // Setup animation callbacks before connecting\n await this.setupAnimationCallbacks();\n await this.provider.connect(config);\n this._isConnected = true;\n }\n\n /**\n * Pre-warm a future RTC connection when the provider supports it.\n * This is best-effort and does not change the connected state.\n */\n async prepareConnection(config: RTCPrepareConnectionConfig): Promise<void> {\n await this.provider.prepareConnection?.(config);\n }\n\n /**\n * Disconnect from RTC server.\n * Stops all tracks and cleans up resources.\n */\n async disconnect(): Promise<void> {\n if (!this._isConnected) {\n return;\n }\n\n // Stop microphone if active\n if (this.microphoneStream) {\n await this.stopPublishing();\n } else if (this.localAudioTrack) {\n // Stop custom audio track\n await this.unpublishAudio();\n }\n\n await this.provider.disconnect();\n this.animationHandler.dispose();\n this._isConnected = false;\n }\n\n /**\n * Publish an audio track to the RTC server.\n *\n * The audio source is controlled externally - you can pass any audio track:\n * - Microphone: `getUserMedia({ audio: true })`\n * - Browser audio: `audioElement.captureStream()`\n * - Web Audio API: `audioContext.createMediaStreamDestination().stream`\n * - Screen share audio: `getDisplayMedia({ audio: true })`\n *\n * @param track - MediaStreamTrack to publish (must be an audio track)\n * @throws Error if not connected or track is invalid\n *\n * @example\n * ```typescript\n * // Microphone\n * const stream = await navigator.mediaDevices.getUserMedia({ audio: true });\n * await player.publishAudio(stream.getAudioTracks()[0]);\n *\n * // Browser audio element\n * const audioEl = document.querySelector('audio');\n * const stream = audioEl.captureStream();\n * await player.publishAudio(stream.getAudioTracks()[0]);\n * ```\n */\n async publishAudio(track: MediaStreamTrack): Promise<void> {\n if (!this._isConnected) {\n throw new Error('Not connected. Please call connect() first.');\n }\n\n if (track.kind !== 'audio') {\n throw new Error('Invalid track: expected audio track');\n }\n\n // Unpublish existing track if any\n if (this.localAudioTrack) {\n await this.unpublishAudio();\n }\n\n this.localAudioTrack = track;\n await this.provider.publishAudioTrack(track);\n }\n\n /**\n * Stop publishing audio.\n * Note: This does NOT stop the track - the caller is responsible for managing the track lifecycle.\n */\n async unpublishAudio(): Promise<void> {\n if (!this.localAudioTrack) {\n return;\n }\n\n await this.provider.unpublishAudioTrack();\n this.localAudioTrack = null;\n }\n\n /**\n * Start publishing microphone audio.\n *\n * This requests microphone permission and publishes it to the RTC session.\n * For custom audio sources (e.g., audio files), use `publishAudio(track)` instead.\n *\n * @throws Error if not connected, permission denied, or no microphone found\n *\n * @example\n * ```typescript\n * await player.startPublishing();\n * // ... later\n * await player.stopPublishing();\n * ```\n */\n async startPublishing(): Promise<void> {\n if (!this._isConnected) {\n throw new Error('Not connected. Please call connect() first.');\n }\n\n if (this.microphoneStream) {\n return; // Already using microphone\n }\n\n // Request microphone permission\n this.microphoneStream = await navigator.mediaDevices.getUserMedia({\n audio: true,\n });\n const track = this.microphoneStream.getAudioTracks()[0];\n\n await this.publishAudio(track);\n }\n\n /**\n * Stop publishing microphone audio and release the microphone.\n *\n * This stops the microphone track and releases the device.\n * If you used `publishAudio()` with a custom track, use `unpublishAudio()` instead.\n */\n async stopPublishing(): Promise<void> {\n if (!this.microphoneStream) {\n return;\n }\n\n await this.unpublishAudio();\n\n // Stop and release microphone\n this.microphoneStream.getTracks().forEach((track) => track.stop());\n this.microphoneStream = null;\n }\n\n /**\n * Get current connection state.\n * @returns Connection state string (e.g., 'connected', 'disconnected')\n */\n getConnectionState(): string {\n return this.provider.getConnectionState();\n }\n\n /**\n * Get the native RTC client object.\n *\n * Returns the underlying RTC client for advanced use cases:\n * - LiveKit: Returns the Room instance\n * - Agora: Returns the IAgoraRTCClient instance\n *\n * @returns The native RTC client object, or null if not connected\n *\n * @example\n * ```typescript\n * // Access LiveKit Room\n * const room = player.getNativeClient() as Room;\n * console.log('Participants:', room?.remoteParticipants.size);\n *\n * // Access Agora Client\n * const client = player.getNativeClient() as IAgoraRTCClient;\n * console.log('State:', client?.connectionState);\n * ```\n */\n getNativeClient(): unknown {\n return this.provider.getNativeClient();\n }\n\n /**\n * Add event listener.\n * @param event - Event name ('connected', 'disconnected', 'error', 'stalled', etc.)\n * @param handler - Event handler function\n */\n on(event: string, handler: (...args: unknown[]) => void): void {\n if (event === 'stalled') {\n // Handle stalled event locally\n if (!this.eventHandlers.has(event)) {\n this.eventHandlers.set(event, new Set());\n }\n this.eventHandlers.get(event)!.add(handler);\n } else {\n // Forward other events to provider\n this.provider.on(event, handler);\n }\n }\n\n /**\n * Remove event listener.\n * @param event - Event name\n * @param handler - Event handler function (must be same reference as added)\n */\n off(event: string, handler: (...args: unknown[]) => void): void {\n if (event === 'stalled') {\n this.eventHandlers.get(event)?.delete(handler);\n } else {\n this.provider.off(event, handler);\n }\n }\n\n /**\n * Emit an event to local listeners.\n * @internal\n */\n private emit(event: string, ...args: unknown[]): void {\n const handlers = this.eventHandlers.get(event);\n if (handlers) {\n handlers.forEach((handler) => {\n try {\n handler(...args);\n } catch (e) {\n logger.error('AvatarPlayer', `Error in ${event} handler:`, e);\n }\n });\n }\n }\n\n /**\n * Reconnect to RTC server using the last connection configuration.\n * Useful for recovering from connection issues or stream stalls.\n *\n * @example\n * ```typescript\n * player.on('stalled', async () => {\n * console.log('Stream stalled, attempting reconnection...');\n * await player.reconnect();\n * });\n * ```\n *\n * @throws Error if no previous connection config exists or reconnection fails\n */\n async reconnect(): Promise<void> {\n if (this.isReconnecting) {\n logger.warn('AvatarPlayer', 'Already attempting reconnection, skipping');\n return;\n }\n\n if (!this.lastConnectionConfig) {\n throw new Error(\n 'Cannot reconnect: no previous connection. Call connect() first.',\n );\n }\n\n this.isReconnecting = true;\n logger.info('AvatarPlayer', 'Attempting reconnection...');\n\n try {\n // Disconnect first if connected\n if (this._isConnected) {\n await this.disconnect();\n }\n\n // Wait a short moment before reconnecting\n await new Promise((resolve) => setTimeout(resolve, 500));\n\n // Reconnect\n await this.connect(this.lastConnectionConfig);\n logger.info('AvatarPlayer', 'Reconnection successful');\n } catch (error) {\n logger.error('AvatarPlayer', 'Reconnection failed:', error);\n throw error;\n } finally {\n this.isReconnecting = false;\n }\n }\n\n /**\n * Create internal renderer adapter that bridges AnimationHandler to AvatarView.\n * @internal\n */\n private createRendererAdapter() {\n const avatarView = this.avatarView;\n\n return {\n renderFrame(flame: KeyframeData | undefined, startIdle?: boolean): void {\n if (startIdle) {\n // Enable idle rendering mode\n avatarView.renderFlame(undefined as unknown as KeyframeData, true);\n } else if (flame) {\n // Render the animation frame\n avatarView.renderFlame(flame);\n }\n },\n\n async generateTransitionFromIdle(\n targetFrame: KeyframeData,\n frameCount: number,\n ): Promise<KeyframeData[]> {\n // Use avatarkit's generateTransition with from=undefined (current idle frame)\n return avatarView.generateTransition({\n to: targetFrame,\n frameCount,\n useLinear: true, // Linear interpolation for idle->speaking\n });\n },\n\n isReady(): boolean {\n return true; // AvatarView is always ready once created\n },\n };\n }\n\n /**\n * Setup animation track callbacks.\n * @internal\n */\n private async setupAnimationCallbacks(): Promise<void> {\n const callbacks: AnimationTrackCallbacks = {\n onAnimationData: (protobufData, metadata) => {\n // Reset tracking only once per session to avoid duplicate start-packet resets.\n if (!this.hasActiveAnimationSession) {\n if (!this.isTrackingPrimed) {\n this.animationHandler.resetTracking();\n }\n\n this.hasActiveAnimationSession = true;\n this.isTrackingPrimed = false;\n\n if (!metadata.isStart) {\n logger.info(\n 'AvatarPlayer',\n `Session start inferred from frame seq=${metadata.frameSeq ?? 'n/a'}`,\n );\n }\n } else if (metadata.isStart) {\n logger.info(\n 'AvatarPlayer',\n `Ignoring duplicate session start frame seq=${metadata.frameSeq ?? 'n/a'}`,\n );\n }\n\n this.animationHandler.handleAnimationData(\n protobufData,\n metadata.frameSeq,\n metadata.isRecovered,\n );\n },\n onTransition: (protobufData, transitionFrameCount) => {\n this.animationHandler.handleTransitionData(\n protobufData,\n transitionFrameCount,\n );\n },\n onTransitionEnd: (protobufData, transitionFrameCount) => {\n this.animationHandler.handleTransitionToIdle(\n protobufData,\n transitionFrameCount,\n );\n },\n onSessionStart: () => {\n // Session started - can be used for UI feedback\n },\n onSessionEnd: () => {\n // Session ended - can be used for UI feedback\n },\n onIdleStart: () => {\n // Idle packets can race with start-transition packets; avoid rendering idle mid-transition.\n if (this.animationHandler.isInTransition()) {\n // Keep session bookkeeping in sync even if visual idle is deferred.\n this.hasActiveAnimationSession = false;\n this.isTrackingPrimed = false;\n logger.info(\n 'AvatarPlayer',\n 'Deferring idleStart while transition is active',\n );\n return;\n }\n\n this.hasActiveAnimationSession = false;\n this.animationHandler.resetTracking();\n this.isTrackingPrimed = true;\n this.animationHandler.startIdle();\n },\n onStreamStats: (stats) => {\n // Log packet loss/recovery stats\n if (\n stats.framesLost > 0 ||\n stats.framesRecovered > 0 ||\n stats.framesDropped > 0\n ) {\n logger.warn(\n 'AvatarPlayer',\n `Stream stats: lost=${stats.framesLost}, recovered=${stats.framesRecovered}, dropped=${stats.framesDropped}, fps=${stats.framesPerSec}`,\n );\n }\n },\n };\n\n await this.provider.subscribeAnimationTrack(callbacks);\n }\n\n /**\n * Setup provider event handlers.\n * @internal\n */\n private setupProviderEvents(): void {\n this.provider.on('connected', () => {\n this._isConnected = true;\n });\n\n this.provider.on('disconnected', () => {\n this._isConnected = false;\n this.hasActiveAnimationSession = false;\n this.isTrackingPrimed = false;\n // Return to idle animation when disconnected\n this.animationHandler.startIdle();\n });\n }\n}\n"],"names":[],"mappings":"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"}
|
package/dist/index3.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
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
|
-
import { BaseProvider } from "./
|
|
5
|
-
import { isLiveKitConfig } from "./index5.js";
|
|
6
|
-
import { VP8Extractor } from "./
|
|
7
|
-
import { getInsertableStreamsMethod } from "./
|
|
4
|
+
import { BaseProvider } from "./index9.js";
|
|
5
|
+
import { isLiveKitPrepareConfig, isLiveKitConfig } from "./index5.js";
|
|
6
|
+
import { VP8Extractor } from "./index10.js";
|
|
7
|
+
import { getInsertableStreamsMethod } from "./index11.js";
|
|
8
8
|
import { logger } from "./index7.js";
|
|
9
9
|
const globalLiveKitProviderRegistry = /* @__PURE__ */ new Set();
|
|
10
10
|
let rtcPeerConnectionPatched = false;
|
|
@@ -29,11 +29,19 @@ function patchRTCPeerConnection() {
|
|
|
29
29
|
listener.handleEvent(event);
|
|
30
30
|
}
|
|
31
31
|
};
|
|
32
|
-
return originalAddEventListener.call(
|
|
32
|
+
return originalAddEventListener.call(
|
|
33
|
+
this,
|
|
34
|
+
type,
|
|
35
|
+
wrappedListener,
|
|
36
|
+
options
|
|
37
|
+
);
|
|
33
38
|
}
|
|
34
39
|
return originalAddEventListener.call(this, type, listener, options);
|
|
35
40
|
};
|
|
36
|
-
const originalOnTrackDescriptor = Object.getOwnPropertyDescriptor(
|
|
41
|
+
const originalOnTrackDescriptor = Object.getOwnPropertyDescriptor(
|
|
42
|
+
RTCPeerConnection.prototype,
|
|
43
|
+
"ontrack"
|
|
44
|
+
);
|
|
37
45
|
if (originalOnTrackDescriptor) {
|
|
38
46
|
Object.defineProperty(RTCPeerConnection.prototype, "ontrack", {
|
|
39
47
|
get: originalOnTrackDescriptor.get,
|
|
@@ -104,6 +112,13 @@ class LiveKitProvider extends BaseProvider {
|
|
|
104
112
|
__publicField(this, "isMicrophoneEnabled", false);
|
|
105
113
|
globalLiveKitProviderRegistry.add(this);
|
|
106
114
|
}
|
|
115
|
+
createRoom(livekit) {
|
|
116
|
+
const { Room } = livekit;
|
|
117
|
+
return new Room({
|
|
118
|
+
videoCaptureDefaults: void 0,
|
|
119
|
+
audioCaptureDefaults: void 0
|
|
120
|
+
});
|
|
121
|
+
}
|
|
107
122
|
/**
|
|
108
123
|
* Load LiveKit SDK dynamically.
|
|
109
124
|
* @internal
|
|
@@ -115,12 +130,31 @@ class LiveKitProvider extends BaseProvider {
|
|
|
115
130
|
try {
|
|
116
131
|
this.livekitSDK = await import("livekit-client");
|
|
117
132
|
return this.livekitSDK;
|
|
118
|
-
} catch
|
|
133
|
+
} catch {
|
|
119
134
|
throw new Error(
|
|
120
135
|
"❌ Failed to load livekit-client.\nPlease ensure it is installed: pnpm add livekit-client"
|
|
121
136
|
);
|
|
122
137
|
}
|
|
123
138
|
}
|
|
139
|
+
async prepareConnection(config) {
|
|
140
|
+
logger.info("LiveKit", "prepareConnection() called with config:", {
|
|
141
|
+
hasUrl: "url" in config,
|
|
142
|
+
hasToken: "token" in config
|
|
143
|
+
});
|
|
144
|
+
if (!isLiveKitPrepareConfig(config)) {
|
|
145
|
+
throw new Error("LiveKitProvider requires url in prepareConnection config");
|
|
146
|
+
}
|
|
147
|
+
const livekit = await this.loadSDK();
|
|
148
|
+
if (!this.room) {
|
|
149
|
+
this.room = this.createRoom(livekit);
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
await this.room.prepareConnection(config.url, config.token);
|
|
153
|
+
logger.info("LiveKit", "prepareConnection() completed");
|
|
154
|
+
} catch (error) {
|
|
155
|
+
logger.warn("LiveKit", "prepareConnection() failed (non-fatal):", error);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
124
158
|
/**
|
|
125
159
|
* Map LiveKit connection state to string.
|
|
126
160
|
* @internal
|
|
@@ -296,7 +330,10 @@ class LiveKitProvider extends BaseProvider {
|
|
|
296
330
|
passive: true
|
|
297
331
|
});
|
|
298
332
|
}
|
|
299
|
-
logger.warn(
|
|
333
|
+
logger.warn(
|
|
334
|
+
"LiveKit",
|
|
335
|
+
"Audio playback blocked by autoplay policy, waiting for user interaction"
|
|
336
|
+
);
|
|
300
337
|
}
|
|
301
338
|
/**
|
|
302
339
|
* Remove user gesture listeners after playback is unlocked.
|
|
@@ -308,7 +345,11 @@ class LiveKitProvider extends BaseProvider {
|
|
|
308
345
|
}
|
|
309
346
|
this.awaitingAudioUnlock = false;
|
|
310
347
|
for (const eventName of this.audioUnlockEventNames) {
|
|
311
|
-
window.removeEventListener(
|
|
348
|
+
window.removeEventListener(
|
|
349
|
+
eventName,
|
|
350
|
+
this.handleAudioUnlockGesture,
|
|
351
|
+
true
|
|
352
|
+
);
|
|
312
353
|
}
|
|
313
354
|
}
|
|
314
355
|
/**
|
|
@@ -320,7 +361,9 @@ class LiveKitProvider extends BaseProvider {
|
|
|
320
361
|
return error.name === "NotAllowedError";
|
|
321
362
|
}
|
|
322
363
|
if (error instanceof Error) {
|
|
323
|
-
return error.name === "NotAllowedError" || /didn't interact|user didn't interact|NotAllowedError/i.test(
|
|
364
|
+
return error.name === "NotAllowedError" || /didn't interact|user didn't interact|NotAllowedError/i.test(
|
|
365
|
+
error.message
|
|
366
|
+
);
|
|
324
367
|
}
|
|
325
368
|
return false;
|
|
326
369
|
}
|
|
@@ -367,7 +410,11 @@ class LiveKitProvider extends BaseProvider {
|
|
|
367
410
|
if (this.isAutoplayBlockedError(error)) {
|
|
368
411
|
this.addAudioUnlockListeners();
|
|
369
412
|
} else {
|
|
370
|
-
logger.warn(
|
|
413
|
+
logger.warn(
|
|
414
|
+
"LiveKit",
|
|
415
|
+
`Failed to call room.startAudio() (${reason}):`,
|
|
416
|
+
error
|
|
417
|
+
);
|
|
371
418
|
}
|
|
372
419
|
return;
|
|
373
420
|
}
|
|
@@ -388,19 +435,26 @@ class LiveKitProvider extends BaseProvider {
|
|
|
388
435
|
config
|
|
389
436
|
});
|
|
390
437
|
if (!isLiveKitConfig(config)) {
|
|
391
|
-
logger.error(
|
|
392
|
-
|
|
438
|
+
logger.error(
|
|
439
|
+
"LiveKit",
|
|
440
|
+
"Config validation failed - missing url or roomName"
|
|
441
|
+
);
|
|
442
|
+
throw new Error(
|
|
443
|
+
"LiveKitProvider requires url and roomName in connection config"
|
|
444
|
+
);
|
|
393
445
|
}
|
|
394
446
|
const livekitConfig = config;
|
|
395
|
-
logger.info(
|
|
447
|
+
logger.info(
|
|
448
|
+
"LiveKit",
|
|
449
|
+
"Config validated, connecting to:",
|
|
450
|
+
livekitConfig.url
|
|
451
|
+
);
|
|
396
452
|
this.removeAudioUnlockListeners();
|
|
397
453
|
const livekit = await this.loadSDK();
|
|
398
454
|
logger.info("LiveKit", "SDK loaded successfully");
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
audioCaptureDefaults: void 0
|
|
403
|
-
});
|
|
455
|
+
if (!this.room) {
|
|
456
|
+
this.room = this.createRoom(livekit);
|
|
457
|
+
}
|
|
404
458
|
this.setConnectionState("connecting");
|
|
405
459
|
this.setupEventListeners(livekit);
|
|
406
460
|
try {
|
|
@@ -408,15 +462,31 @@ class LiveKitProvider extends BaseProvider {
|
|
|
408
462
|
await this.room.connect(livekitConfig.url, livekitConfig.token);
|
|
409
463
|
logger.info("LiveKit", "Room connected, state:", this.room.state);
|
|
410
464
|
logger.info("LiveKit", "Room name:", this.room.name);
|
|
411
|
-
logger.info(
|
|
412
|
-
|
|
465
|
+
logger.info(
|
|
466
|
+
"LiveKit",
|
|
467
|
+
"Local participant:",
|
|
468
|
+
(_a = this.room.localParticipant) == null ? void 0 : _a.identity
|
|
469
|
+
);
|
|
470
|
+
logger.info(
|
|
471
|
+
"LiveKit",
|
|
472
|
+
"Remote participants:",
|
|
473
|
+
this.room.remoteParticipants.size
|
|
474
|
+
);
|
|
413
475
|
this.room.remoteParticipants.forEach((participant, sid) => {
|
|
414
|
-
logger.info(
|
|
476
|
+
logger.info(
|
|
477
|
+
"LiveKit",
|
|
478
|
+
`Remote participant: ${participant.identity}, sid: ${sid}`
|
|
479
|
+
);
|
|
415
480
|
participant.trackPublications.forEach((pub, trackSid) => {
|
|
416
|
-
logger.info(
|
|
481
|
+
logger.info(
|
|
482
|
+
"LiveKit",
|
|
483
|
+
` Track: ${pub.trackName}, kind: ${pub.kind}, subscribed: ${pub.isSubscribed}, sid: ${trackSid}`
|
|
484
|
+
);
|
|
417
485
|
});
|
|
418
486
|
});
|
|
419
|
-
this.setConnectionState(
|
|
487
|
+
this.setConnectionState(
|
|
488
|
+
this.mapLiveKitConnectionState(livekit, this.room.state)
|
|
489
|
+
);
|
|
420
490
|
} catch (error) {
|
|
421
491
|
logger.error("LiveKit", "Connection failed:", error);
|
|
422
492
|
this.setConnectionState("failed");
|
|
@@ -444,7 +514,10 @@ class LiveKitProvider extends BaseProvider {
|
|
|
444
514
|
logger.info("LiveKit", `Participant connected: ${participant.identity}`);
|
|
445
515
|
});
|
|
446
516
|
room.on(RoomEvent.ParticipantDisconnected, (participant) => {
|
|
447
|
-
logger.warn(
|
|
517
|
+
logger.warn(
|
|
518
|
+
"LiveKit",
|
|
519
|
+
`Participant disconnected: ${participant.identity}`
|
|
520
|
+
);
|
|
448
521
|
});
|
|
449
522
|
room.on(RoomEvent.TrackPublished, (publication, participant) => {
|
|
450
523
|
logger.info("LiveKit", "TrackPublished:", {
|
|
@@ -458,14 +531,22 @@ class LiveKitProvider extends BaseProvider {
|
|
|
458
531
|
this.setConnectionState("disconnected");
|
|
459
532
|
this.emit("disconnected");
|
|
460
533
|
});
|
|
461
|
-
room.on(
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
534
|
+
room.on(
|
|
535
|
+
RoomEvent.AudioPlaybackStatusChanged,
|
|
536
|
+
(canPlaybackAudio) => {
|
|
537
|
+
logger.info(
|
|
538
|
+
"LiveKit",
|
|
539
|
+
`Audio playback status changed: ${canPlaybackAudio ? "enabled" : "blocked"}`
|
|
540
|
+
);
|
|
541
|
+
if (canPlaybackAudio) {
|
|
542
|
+
void this.ensureAudioPlaybackUnlocked(
|
|
543
|
+
"audio-playback-status-changed"
|
|
544
|
+
);
|
|
545
|
+
} else {
|
|
546
|
+
this.addAudioUnlockListeners();
|
|
547
|
+
}
|
|
467
548
|
}
|
|
468
|
-
|
|
549
|
+
);
|
|
469
550
|
this.room.on(
|
|
470
551
|
RoomEvent.TrackSubscribed,
|
|
471
552
|
(track, publication, participant) => {
|
|
@@ -484,7 +565,9 @@ class LiveKitProvider extends BaseProvider {
|
|
|
484
565
|
audioElement.autoplay = true;
|
|
485
566
|
audioElement.controls = false;
|
|
486
567
|
audioElement.style.display = "none";
|
|
487
|
-
const previousAudioElement = this.audioElements.get(
|
|
568
|
+
const previousAudioElement = this.audioElements.get(
|
|
569
|
+
participant.identity
|
|
570
|
+
);
|
|
488
571
|
if (previousAudioElement && previousAudioElement !== audioElement) {
|
|
489
572
|
previousAudioElement.remove();
|
|
490
573
|
}
|
|
@@ -498,13 +581,24 @@ class LiveKitProvider extends BaseProvider {
|
|
|
498
581
|
(_b = (_a = this.audioCallbacks).onAudioReceived) == null ? void 0 : _b.call(_a, participant);
|
|
499
582
|
}
|
|
500
583
|
} else if (track.kind === Track.Kind.Video) {
|
|
501
|
-
logger.info(
|
|
584
|
+
logger.info(
|
|
585
|
+
"LiveKit",
|
|
586
|
+
"Video track received, setting up animation transform..."
|
|
587
|
+
);
|
|
502
588
|
if (mediaStreamTrack) {
|
|
503
|
-
const receiver = this.findVideoReceiverByTrackId(
|
|
589
|
+
const receiver = this.findVideoReceiverByTrackId(
|
|
590
|
+
mediaStreamTrack.id
|
|
591
|
+
);
|
|
504
592
|
if (receiver) {
|
|
505
|
-
this.receiverParticipantMap.set(receiver, {
|
|
593
|
+
this.receiverParticipantMap.set(receiver, {
|
|
594
|
+
participant,
|
|
595
|
+
trackName
|
|
596
|
+
});
|
|
506
597
|
if (!receiver.transform || !this.transformedReceivers.has(receiver)) {
|
|
507
|
-
this.applyAnimationReceiverTransform(
|
|
598
|
+
this.applyAnimationReceiverTransform(
|
|
599
|
+
receiver,
|
|
600
|
+
mediaStreamTrack
|
|
601
|
+
);
|
|
508
602
|
}
|
|
509
603
|
}
|
|
510
604
|
}
|
|
@@ -570,9 +664,14 @@ class LiveKitProvider extends BaseProvider {
|
|
|
570
664
|
if (((_a = publication.trackName) == null ? void 0 : _a.includes("animation")) && publication.track) {
|
|
571
665
|
const mediaStreamTrack = publication.track.mediaStreamTrack;
|
|
572
666
|
if (mediaStreamTrack) {
|
|
573
|
-
const receiver = this.findVideoReceiverByTrackId(
|
|
667
|
+
const receiver = this.findVideoReceiverByTrackId(
|
|
668
|
+
mediaStreamTrack.id
|
|
669
|
+
);
|
|
574
670
|
if (receiver) {
|
|
575
|
-
this.applyAnimationReceiverTransform(
|
|
671
|
+
this.applyAnimationReceiverTransform(
|
|
672
|
+
receiver,
|
|
673
|
+
mediaStreamTrack
|
|
674
|
+
);
|
|
576
675
|
}
|
|
577
676
|
}
|
|
578
677
|
}
|
|
@@ -652,12 +751,12 @@ class LiveKitProvider extends BaseProvider {
|
|
|
652
751
|
}
|
|
653
752
|
/**
|
|
654
753
|
* Get the native LiveKit Room instance.
|
|
655
|
-
*
|
|
754
|
+
*
|
|
656
755
|
* Allows advanced users to access LiveKit-specific features
|
|
657
756
|
* not exposed through the unified API.
|
|
658
|
-
*
|
|
757
|
+
*
|
|
659
758
|
* @returns The LiveKit Room instance, or null if not connected
|
|
660
|
-
*
|
|
759
|
+
*
|
|
661
760
|
* @example
|
|
662
761
|
* ```typescript
|
|
663
762
|
* const room = provider.getNativeClient();
|