@spatialwalk/avatarkit-rtc 1.0.0-beta.6 → 1.0.0-beta.8
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/dist/assets/animation-worker-CdhDm7lL.js.map +1 -0
- package/dist/core/AvatarPlayer.d.ts.map +1 -1
- package/dist/index11.js +64 -346
- package/dist/index11.js.map +1 -1
- package/dist/index12.js +14 -104
- package/dist/index12.js.map +1 -1
- package/dist/index13.js +386 -14
- package/dist/index13.js.map +1 -1
- package/dist/index15.js +1 -1
- package/dist/index2.js +34 -2
- package/dist/index2.js.map +1 -1
- package/dist/index3.js +2 -2
- package/dist/index4.js +1 -1
- package/dist/index6.js +134 -17
- package/dist/index6.js.map +1 -1
- package/dist/providers/livekit/animation-worker.d.ts.map +1 -1
- package/package.json +4 -4
- package/dist/assets/animation-worker-CUXZycUw.js.map +0 -1
package/dist/index6.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index6.js","sources":["../src/core/AnimationHandler.ts"],"sourcesContent":["/**\n * Animation Handler - Orchestrates animation playback and transitions.\n *\n * This module handles:\n * - Animation frame rendering\n * - Transition playback from idle to animation and back\n * - Frame timing at 25fps\n * - Session state tracking\n *\n * The handler relies on server-sent packet flags (Transition, TransitionEnd, Idle)\n * to determine when to generate transitions, rather than maintaining complex internal state.\n *\n * @internal\n * @packageDocumentation\n */\n\nimport type { Flame } from '../proto/animation';\nimport { decodeAnimationKeyframes } from '../proto/animation';\nimport { logger } from '../utils';\n\n/**\n * Interface for decoding protobuf animation data.\n * Allows custom decoder implementation if needed.\n * @internal\n */\nexport type AnimationDecoder = (protobufData: ArrayBuffer) => Flame[] | null;\n\n/**\n * Interface that applications must implement to render avatar frames.\n * The SDK calls these methods to control the avatar.\n * @internal\n */\nexport interface AvatarRenderer {\n /**\n * Render a single animation frame.\n * @param flame - The frame to render, or undefined to render idle\n * @param startIdle - If true and flame is undefined, start idle animation loop\n */\n renderFrame(flame: Flame | undefined, startIdle?: boolean): void;\n\n /**\n * Generate transition frames from idle position to target frame.\n * @param targetFrame - The target frame to transition to\n * @param frameCount - Number of transition frames to generate\n * @returns Promise resolving to array of transition frames\n */\n generateTransitionFromIdle(\n targetFrame: Flame,\n frameCount: number\n ): Promise<Flame[]>;\n\n /**\n * Check if the renderer is available and ready.\n * @returns true if renderer is ready to accept frames\n */\n isReady(): boolean;\n}\n\n/**\n * Configuration for AnimationHandler.\n * @internal\n */\nexport interface AnimationHandlerConfig {\n /**\n * Number of transition frames when starting animation.\n * Default: 8 (~320ms at 25fps)\n */\n transitionStartFrameCount?: number;\n\n /**\n * Number of transition frames when ending animation.\n * Default: 12 (~480ms at 25fps)\n */\n transitionEndFrameCount?: number;\n\n /**\n * Custom decoder function.\n * Uses built-in protobuf decoder if not provided.\n */\n decoder?: AnimationDecoder;\n\n /**\n * Callback when data stream stalls and fallback to idle is triggered.\n * Called when no animation frames received for 3 seconds during speaking session.\n */\n onStreamStalled?: () => void;\n\n /**\n * Enable jitter buffer for smoother 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 * Also controls how long to wait for a missing frame before skipping ahead.\n * Default: 80 (2 frames at 25fps)\n */\n maxBufferDelayMs?: number;\n}\n\n/**\n * Jitter buffer state machine.\n * @internal\n */\ntype BufferState = 'direct' | 'filling' | 'draining' | 'starved';\n\n/**\n * A frame held in the jitter buffer awaiting playback.\n * @internal\n */\ninterface BufferedFrame {\n flame: Flame;\n seq: number;\n receivedAt: number;\n}\n\n/**\n * AnimationHandler manages all animation playback and transition logic.\n *\n * @internal\n * This class is used internally by AvatarPlayer.\n * Applications should not instantiate this class directly.\n */\nexport class AnimationHandler {\n /** @internal */\n private renderer: AvatarRenderer;\n /** @internal */\n private decoder: AnimationDecoder;\n /** @internal */\n private config: Required<Omit<AnimationHandlerConfig, 'decoder' | 'onStreamStalled'>>;\n\n // Frame tracking\n /** @internal */\n private animationFrameCount = 0;\n /** @internal */\n private lastRenderedFrameSeq = -1;\n /** @internal */\n private renderedFrameCount = 0;\n\n // Transition state\n /** @internal */\n private isPlayingTransition = false;\n /** @internal */\n private isTransitioningToIdle = false;\n /** @internal */\n private transitionFrames: Flame[] = [];\n /** @internal */\n private transitionFrameIndex = 0;\n /** @internal */\n private transitionTimeoutId: ReturnType<typeof setTimeout> | null = null;\n\n // Guards against race conditions during async transition generation\n /** @internal */\n private isGeneratingStartTransition = false;\n /** @internal */\n private isGeneratingEndTransition = false;\n\n // Session-level flags to prevent duplicate handling\n // These are reset in resetTracking() for new sessions\n /** @internal */\n private hasHandledTransitionStart = false;\n /** @internal */\n private hasHandledTransitionEnd = false;\n\n // Watchdog for detecting stalled data stream\n /** @internal */\n private lastFrameReceivedTime = 0;\n /** @internal */\n private isInSession = false;\n /** @internal */\n private watchdogTimer: ReturnType<typeof setInterval> | null = null;\n /** @internal */\n private hasReportedStall = false;\n /** @internal */\n private static readonly STALL_TIMEOUT_MS = 3000;\n /** @internal */\n private onStreamStalledCallback: (() => void) | null = null;\n /** @internal */\n private isStalledFallback = false; // True when fallback to idle was triggered due to stall\n\n // Playback stats\n /** @internal */\n private playbackStatsTimer: ReturnType<typeof setInterval> | null = null;\n /** @internal */\n private playbackFrameCount = 0;\n /** @internal */\n private playbackFrameTimestamps: number[] = [];\n /** @internal */\n private playbackGapCount = 0;\n /** @internal */\n private playbackExpectedSeq = -1;\n\n // Jitter buffer\n /** @internal */\n private bufferState: BufferState = 'direct';\n /** @internal */\n private frameBuffer = new Map<number, BufferedFrame>();\n /** @internal */\n private bufferNextSeq = -1;\n /** @internal */\n private bufferDrainTimer: ReturnType<typeof setTimeout> | null = null;\n /** @internal */\n private bufferLastDrainTime = 0;\n /** @internal */\n private static readonly BUFFER_MAX_SIZE = 4;\n /** @internal */\n private static readonly BUFFER_INITIAL_FILL = 2;\n /** @internal */\n private static readonly BUFFER_FRAME_INTERVAL_MS = 40;\n\n /**\n * @internal\n */\n constructor(renderer: AvatarRenderer, config: AnimationHandlerConfig = {}) {\n this.renderer = renderer;\n this.decoder = config.decoder ?? decodeAnimationKeyframes;\n this.config = {\n transitionStartFrameCount: config.transitionStartFrameCount ?? 8,\n transitionEndFrameCount: config.transitionEndFrameCount ?? 12,\n enableJitterBuffer: config.enableJitterBuffer ?? true,\n maxBufferDelayMs: config.maxBufferDelayMs ?? 80,\n };\n this.onStreamStalledCallback = config.onStreamStalled ?? null;\n }\n\n /**\n * Handle animation data received from RTC provider.\n * @param protobufData - Raw protobuf bytes\n * @param frameSeq - Frame sequence number (optional)\n * @param isRecovered - Whether this frame was recovered via ALR\n * @internal\n */\n handleAnimationData(\n protobufData: ArrayBuffer,\n frameSeq?: number,\n isRecovered?: boolean\n ): void {\n // If we were in transition, stop it and play normal animation\n if (this.isPlayingTransition) {\n this.stopTransition();\n }\n\n // Update watchdog state\n const now = Date.now();\n if (this.hasReportedStall) {\n // Recovered from stall\n const stallDuration = now - this.lastFrameReceivedTime;\n logger.info('AnimationHandler', `Data stream resumed after ${stallDuration}ms stall`);\n this.hasReportedStall = false;\n }\n // If we were in stalled fallback, clear the flag (data has resumed, no transition needed)\n if (this.isStalledFallback) {\n logger.info('AnimationHandler', 'Resuming from stall fallback, rendering directly without transition');\n this.isStalledFallback = false;\n }\n this.lastFrameReceivedTime = now;\n\n this.animationFrameCount++;\n\n // Decode\n const keyframes = this.decoder(protobufData);\n if (!keyframes || keyframes.length === 0) {\n return;\n }\n\n // Jitter buffer path: insert into buffer instead of rendering directly\n if (this.config.enableJitterBuffer && frameSeq !== undefined) {\n this.bufferFrame(keyframes[0], frameSeq);\n return;\n }\n\n // Direct path (no buffer)\n this.renderedFrameCount++;\n\n // Check for out-of-order delivery\n if (frameSeq !== undefined && this.lastRenderedFrameSeq !== -1) {\n if (frameSeq < this.lastRenderedFrameSeq) {\n // Out-of-order frame - discard (already rendered a later frame)\n logger.warn(\n 'AnimationHandler',\n `OUT-OF-ORDER: seq=${frameSeq}, lastRendered=${this.lastRenderedFrameSeq}${isRecovered ? ' [RECOVERED]' : ''}, discarding`\n );\n return;\n } else if (frameSeq === this.lastRenderedFrameSeq) {\n // Duplicate frame - skip silently (expected when server sends redundant frames)\n return;\n } else if (frameSeq > this.lastRenderedFrameSeq + 1) {\n const gap = frameSeq - this.lastRenderedFrameSeq - 1;\n logger.warn(\n 'AnimationHandler',\n `GAP: ${gap} frame(s) between ${this.lastRenderedFrameSeq} and ${frameSeq}${isRecovered ? ' [RECOVERED]' : ''}`\n );\n }\n }\n\n if (frameSeq !== undefined) {\n this.lastRenderedFrameSeq = frameSeq;\n }\n\n // Render directly\n this.renderer.renderFrame(keyframes[0]);\n\n // Collect playback stats\n this.playbackFrameTimestamps.push(performance.now());\n this.playbackFrameCount++;\n if (frameSeq !== undefined) {\n if (this.playbackExpectedSeq >= 0 && frameSeq > this.playbackExpectedSeq) {\n this.playbackGapCount += frameSeq - this.playbackExpectedSeq;\n }\n this.playbackExpectedSeq = frameSeq + 1;\n }\n }\n\n /**\n * Handle transition packet - generate and play transition from idle to target.\n * Only starts transition on the first packet; subsequent packets are ignored while transitioning.\n * @param protobufData - Protobuf data containing target frame\n * @param frameCount - Number of transition frames (overrides config if provided)\n * @internal\n */\n async handleTransitionData(\n protobufData: ArrayBuffer,\n frameCount?: number\n ): Promise<void> {\n // Ignore if we've already handled a transition start for this session\n if (this.hasHandledTransitionStart) {\n return;\n }\n // Ignore if already playing or generating a start transition\n if (this.isPlayingTransition && !this.isTransitioningToIdle) {\n return;\n }\n if (this.isGeneratingStartTransition) {\n return;\n }\n\n if (!this.renderer.isReady()) {\n logger.warn('AnimationHandler', 'Renderer not ready for transition');\n return;\n }\n\n // Decode target frame\n const keyframes = this.decoder(protobufData);\n if (!keyframes || keyframes.length === 0) {\n logger.warn('AnimationHandler', 'No target keyframe in transition data');\n return;\n }\n\n // Mark that we've handled transition start for this session\n this.hasHandledTransitionStart = true;\n // Reset transition end flag since we're starting a new transition\n this.hasHandledTransitionEnd = false;\n\n // Start session and watchdog\n this.isInSession = true;\n this.lastFrameReceivedTime = Date.now();\n this.hasReportedStall = false;\n this.startWatchdog();\n this.startPlaybackStats();\n\n const targetFrame = keyframes[0];\n const frames = frameCount ?? this.config.transitionStartFrameCount;\n logger.info('AnimationHandler', `Generating ${frames} transition frames to target`);\n\n // Set guard flag before async operation\n this.isGeneratingStartTransition = true;\n\n try {\n // Generate transition frames from idle to target\n const transitionFrames = await this.renderer.generateTransitionFromIdle(\n targetFrame,\n frames\n );\n logger.info('AnimationHandler', `Generated ${transitionFrames.length} transition frames`);\n\n // Start playing transition frames at 25fps\n this.isPlayingTransition = true;\n this.isTransitioningToIdle = false;\n this.transitionFrames = transitionFrames;\n this.transitionFrameIndex = 0;\n\n this.playTransitionFrame();\n } catch (error) {\n logger.error('AnimationHandler', 'Failed to generate transition:', error);\n // Fallback: render target directly\n this.renderer.renderFrame(targetFrame);\n } finally {\n this.isGeneratingStartTransition = false;\n }\n }\n\n /**\n * Handle transition end - generate and play reverse transition back to idle.\n * Only starts transition on the first packet; subsequent packets are ignored while transitioning.\n * @param protobufData - Protobuf data containing last animation frame\n * @param frameCount - Number of transition frames (overrides config if provided)\n * @internal\n */\n async handleTransitionToIdle(\n protobufData: ArrayBuffer,\n frameCount?: number\n ): Promise<void> {\n // Ignore if we've already handled a transition end for this session\n if (this.hasHandledTransitionEnd) {\n return;\n }\n // Ignore if already playing or generating an end transition\n if (this.isPlayingTransition && this.isTransitioningToIdle) {\n return;\n }\n if (this.isGeneratingEndTransition) {\n return;\n }\n\n if (!this.renderer.isReady()) {\n logger.warn('AnimationHandler', 'Renderer not ready for transition to idle');\n this.renderer.renderFrame(undefined, true);\n return;\n }\n\n // Decode last animation frame\n const keyframes = this.decoder(protobufData);\n if (!keyframes || keyframes.length === 0) {\n logger.warn('AnimationHandler', 'No last keyframe in transition end data, starting idle directly');\n this.renderer.renderFrame(undefined, true);\n return;\n }\n\n // Mark that we've handled transition end for this session\n this.hasHandledTransitionEnd = true;\n\n const lastFrame = keyframes[0];\n const frames = frameCount ?? this.config.transitionEndFrameCount;\n logger.info('AnimationHandler', `Generating ${frames} reverse transition frames to idle`);\n\n // Set guard flag before async operation\n this.isGeneratingEndTransition = true;\n\n try {\n // Generate transition frames from idle to last frame, then reverse\n const transitionFrames = await this.renderer.generateTransitionFromIdle(\n lastFrame,\n frames\n );\n logger.info('AnimationHandler', `Generated ${transitionFrames.length} transition frames, reversing for playback`);\n\n // Reverse frames to play from last animation frame back to idle\n const reversedFrames = transitionFrames.slice().reverse();\n\n // Start playing reversed transition at 25fps\n this.isPlayingTransition = true;\n this.isTransitioningToIdle = true;\n this.transitionFrames = reversedFrames;\n this.transitionFrameIndex = 0;\n\n this.playTransitionFrame();\n } catch (error) {\n logger.error('AnimationHandler', 'Failed to generate reverse transition:', error);\n this.renderer.renderFrame(undefined, true);\n } finally {\n this.isGeneratingEndTransition = false;\n }\n }\n\n /**\n * Start idle animation.\n * @internal\n */\n startIdle(): void {\n this.isInSession = false;\n this.hasReportedStall = false;\n this.renderer.renderFrame(undefined, true);\n }\n\n /**\n * Reset animation frame tracking (call on session start).\n * @internal\n */\n resetTracking(): void {\n this.lastRenderedFrameSeq = -1;\n this.renderedFrameCount = 0;\n this.animationFrameCount = 0;\n // Reset session-level transition flags for new session\n this.hasHandledTransitionStart = false;\n this.hasHandledTransitionEnd = false;\n this.resetPlaybackStats();\n this.flushBuffer();\n logger.info('AnimationHandler', 'Frame tracking reset');\n }\n\n /**\n * Check if currently playing transition frames.\n * @internal\n */\n isInTransition(): boolean {\n return this.isPlayingTransition;\n }\n\n /**\n * Stop transition playback.\n * @internal\n */\n stopTransition(): void {\n if (\n this.isPlayingTransition ||\n this.isGeneratingStartTransition ||\n this.isGeneratingEndTransition\n ) {\n logger.info('AnimationHandler', 'Stopping transition playback');\n }\n this.isPlayingTransition = false;\n this.isTransitioningToIdle = false;\n this.isGeneratingStartTransition = false;\n this.isGeneratingEndTransition = false;\n // Note: We intentionally do NOT reset hasHandledTransitionStart/End here\n // Those are session-level flags, only reset in resetTracking()\n this.transitionFrames = [];\n this.transitionFrameIndex = 0;\n if (this.transitionTimeoutId !== null) {\n clearTimeout(this.transitionTimeoutId);\n this.transitionTimeoutId = null;\n }\n this.flushBuffer();\n }\n\n /**\n * Clean up resources.\n * @internal\n */\n dispose(): void {\n this.stopTransition();\n this.stopWatchdog();\n }\n\n /**\n * Start the watchdog timer to detect stalled data streams.\n * @internal\n */\n private startWatchdog(): void {\n if (this.watchdogTimer) {\n return; // Already running\n }\n\n this.watchdogTimer = setInterval(() => {\n if (!this.isInSession) {\n return;\n }\n\n // Don't check during transition playback (we're playing generated frames, not receiving)\n if (this.isPlayingTransition) {\n return;\n }\n\n const elapsed = Date.now() - this.lastFrameReceivedTime;\n if (elapsed > AnimationHandler.STALL_TIMEOUT_MS && !this.hasReportedStall) {\n logger.error(\n 'AnimationHandler',\n `Data stream stalled: no frames received for ${elapsed}ms, falling back to idle`\n );\n this.hasReportedStall = true;\n this.isStalledFallback = true;\n\n // Trigger fallback to idle\n this.startIdle();\n\n // Notify external listener\n if (this.onStreamStalledCallback) {\n try {\n this.onStreamStalledCallback();\n } catch (e) {\n logger.error('AnimationHandler', 'Error in onStreamStalled callback:', e);\n }\n }\n }\n }, 1000); // Check every 1 second\n }\n\n /**\n * Stop the watchdog timer.\n * @internal\n */\n private stopWatchdog(): void {\n if (this.watchdogTimer) {\n clearInterval(this.watchdogTimer);\n this.watchdogTimer = null;\n }\n this.hasReportedStall = false;\n this.isStalledFallback = false;\n this.stopPlaybackStats();\n }\n\n /**\n * Start the playback stats reporting timer.\n * @internal\n */\n private startPlaybackStats(): void {\n if (this.playbackStatsTimer) {\n return; // Already running\n }\n this.resetPlaybackStats();\n this.playbackStatsTimer = setInterval(() => {\n this.reportPlaybackStats();\n }, 1000);\n }\n\n /**\n * Stop the playback stats reporting timer.\n * @internal\n */\n private stopPlaybackStats(): void {\n if (this.playbackStatsTimer) {\n clearInterval(this.playbackStatsTimer);\n this.playbackStatsTimer = null;\n }\n this.resetPlaybackStats();\n }\n\n /**\n * Reset playback stats counters.\n * @internal\n */\n private resetPlaybackStats(): void {\n this.playbackFrameCount = 0;\n this.playbackFrameTimestamps = [];\n this.playbackGapCount = 0;\n this.playbackExpectedSeq = -1;\n }\n\n /**\n * Report playback stats (called every 1s by timer).\n * Logs FPS, frame loss rate, and playback jitter.\n * @internal\n */\n private reportPlaybackStats(): void {\n // Skip reporting during transitions (locally generated frames)\n if (this.isPlayingTransition) {\n this.resetPlaybackStats();\n return;\n }\n\n // Skip if no frames were rendered this interval\n if (this.playbackFrameCount === 0) {\n return;\n }\n\n const fps = this.playbackFrameCount;\n const totalExpected = this.playbackFrameCount + this.playbackGapCount;\n const lossRate = totalExpected > 0 ? (this.playbackGapCount / totalExpected) * 100 : 0;\n\n // Calculate jitter: std deviation of inter-frame intervals\n let jitter = 0;\n if (this.playbackFrameTimestamps.length >= 2) {\n const intervals: number[] = [];\n for (let i = 1; i < this.playbackFrameTimestamps.length; i++) {\n intervals.push(this.playbackFrameTimestamps[i] - this.playbackFrameTimestamps[i - 1]);\n }\n const mean = intervals.reduce((a, b) => a + b, 0) / intervals.length;\n const variance = intervals.reduce((sum, v) => sum + (v - mean) ** 2, 0) / intervals.length;\n jitter = Math.sqrt(variance);\n }\n\n logger.info(\n 'AnimationHandler',\n `Playback stats: fps=${fps}, lossRate=${lossRate.toFixed(1)}%, jitter=${jitter.toFixed(1)}ms`\n );\n\n // Reset counters for next interval\n this.playbackFrameCount = 0;\n this.playbackFrameTimestamps = [];\n this.playbackGapCount = 0;\n }\n\n // ── Jitter Buffer ──\n\n /**\n * Insert a decoded frame into the jitter buffer.\n * Handles dedup, enforces max size, and drives the buffer state machine.\n * @internal\n */\n private bufferFrame(flame: Flame, seq: number): void {\n // Dedup — server sends each frame twice for resilience\n if (this.frameBuffer.has(seq)) {\n return;\n }\n\n this.frameBuffer.set(seq, { flame, seq, receivedAt: performance.now() });\n\n // Enforce max buffer size — drop oldest when full\n if (this.frameBuffer.size > AnimationHandler.BUFFER_MAX_SIZE) {\n let oldestSeq = Infinity;\n for (const k of this.frameBuffer.keys()) {\n if (k < oldestSeq) oldestSeq = k;\n }\n this.frameBuffer.delete(oldestSeq);\n }\n\n // State machine transitions\n switch (this.bufferState) {\n case 'direct':\n this.bufferState = 'filling';\n if (this.bufferNextSeq < 0) {\n this.bufferNextSeq = seq;\n }\n logger.info('AnimationHandler', `Jitter buffer: filling (first frame seq=${seq})`);\n if (this.frameBuffer.size >= AnimationHandler.BUFFER_INITIAL_FILL) {\n this.startBufferDrain();\n }\n break;\n case 'filling':\n if (this.frameBuffer.size >= AnimationHandler.BUFFER_INITIAL_FILL) {\n this.startBufferDrain();\n }\n break;\n case 'starved':\n this.startBufferDrain();\n break;\n case 'draining':\n // Already draining, frame will be picked up by drain loop\n break;\n }\n }\n\n /**\n * Begin draining the buffer at 25fps.\n * @internal\n */\n private startBufferDrain(): void {\n this.bufferState = 'draining';\n if (this.bufferNextSeq < 0) {\n let minSeq = Infinity;\n for (const k of this.frameBuffer.keys()) {\n if (k < minSeq) minSeq = k;\n }\n this.bufferNextSeq = minSeq;\n }\n logger.info('AnimationHandler', `Jitter buffer: draining (${this.frameBuffer.size} frames buffered)`);\n this.bufferLastDrainTime = performance.now();\n this.drainBufferFrame();\n }\n\n /**\n * Drain one frame from the buffer and schedule the next drain.\n * Handles missing frames (skip-ahead after maxBufferDelayMs) and starvation.\n * @internal\n */\n private drainBufferFrame(): void {\n this.bufferDrainTimer = null;\n\n if (this.bufferState !== 'draining') {\n return;\n }\n\n const frame = this.frameBuffer.get(this.bufferNextSeq);\n\n if (frame) {\n // Expected frame found — render it\n this.renderBufferedFrame(frame);\n this.frameBuffer.delete(this.bufferNextSeq);\n this.bufferNextSeq++;\n } else if (this.frameBuffer.size > 0) {\n // Expected frame missing — check if we should skip ahead\n let oldestSeq = Infinity;\n for (const k of this.frameBuffer.keys()) {\n if (k < oldestSeq) oldestSeq = k;\n }\n const oldestFrame = this.frameBuffer.get(oldestSeq)!;\n const waitTime = performance.now() - oldestFrame.receivedAt;\n\n if (waitTime > this.config.maxBufferDelayMs) {\n // Oldest frame has waited too long — skip ahead\n const gap = oldestSeq - this.bufferNextSeq;\n this.playbackGapCount += gap;\n logger.warn(\n 'AnimationHandler',\n `Jitter buffer: skipping ${gap} frame(s) from seq ${this.bufferNextSeq} to ${oldestSeq}`\n );\n this.renderBufferedFrame(oldestFrame);\n this.frameBuffer.delete(oldestSeq);\n this.bufferNextSeq = oldestSeq + 1;\n }\n // else: wait for the missing frame (render nothing this cycle)\n } else {\n // Buffer empty — starvation\n this.bufferState = 'starved';\n logger.warn('AnimationHandler', 'Jitter buffer: starved, pausing drain');\n return; // Don't schedule next drain\n }\n\n // Schedule next drain with drift correction\n const now = performance.now();\n const nextTarget = this.bufferLastDrainTime + AnimationHandler.BUFFER_FRAME_INTERVAL_MS;\n const delay = Math.max(0, nextTarget - now);\n this.bufferLastDrainTime = nextTarget;\n this.bufferDrainTimer = setTimeout(() => this.drainBufferFrame(), delay);\n }\n\n /**\n * Render a single frame from the buffer and collect playback stats.\n * @internal\n */\n private renderBufferedFrame(frame: BufferedFrame): void {\n this.renderer.renderFrame(frame.flame);\n this.lastRenderedFrameSeq = frame.seq;\n this.renderedFrameCount++;\n\n // Playback stats\n this.playbackFrameTimestamps.push(performance.now());\n this.playbackFrameCount++;\n }\n\n /**\n * Flush the jitter buffer, stop the drain loop, and revert to direct mode.\n * @internal\n */\n private flushBuffer(): void {\n this.frameBuffer.clear();\n this.bufferState = 'direct';\n this.bufferNextSeq = -1;\n this.bufferLastDrainTime = 0;\n if (this.bufferDrainTimer !== null) {\n clearTimeout(this.bufferDrainTimer);\n this.bufferDrainTimer = null;\n }\n }\n\n /**\n * Play a single transition frame and schedule the next one.\n * @internal\n */\n private playTransitionFrame(): void {\n if (\n !this.isPlayingTransition ||\n this.transitionFrameIndex >= this.transitionFrames.length\n ) {\n // Transition complete\n const wasTransitioningToIdle = this.isTransitioningToIdle;\n this.isPlayingTransition = false;\n this.isTransitioningToIdle = false;\n this.transitionFrames = [];\n this.transitionFrameIndex = 0;\n if (this.transitionTimeoutId !== null) {\n clearTimeout(this.transitionTimeoutId);\n this.transitionTimeoutId = null;\n }\n logger.info('AnimationHandler', 'Transition playback complete');\n\n // If transitioning to idle, start idle animation\n if (wasTransitioningToIdle) {\n logger.info('AnimationHandler', 'Starting idle animation after transition');\n this.renderer.renderFrame(undefined, true);\n }\n return;\n }\n\n if (!this.renderer.isReady()) {\n this.isPlayingTransition = false;\n this.isTransitioningToIdle = false;\n return;\n }\n\n const frame = this.transitionFrames[this.transitionFrameIndex];\n this.renderer.renderFrame(frame);\n this.transitionFrameIndex++;\n\n // Schedule next frame at 25fps (40ms)\n this.transitionTimeoutId = setTimeout(() => {\n if (this.isPlayingTransition) {\n this.playTransitionFrame();\n }\n }, 40);\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index6.js","sources":["../src/core/AnimationHandler.ts"],"sourcesContent":["/**\n * Animation Handler - Orchestrates animation playback and transitions.\n *\n * This module handles:\n * - Animation frame rendering\n * - Transition playback from idle to animation and back\n * - Frame timing at 25fps\n * - Session state tracking\n *\n * The handler relies on server-sent packet flags (Transition, TransitionEnd, Idle)\n * to determine when to generate transitions, rather than maintaining complex internal state.\n *\n * @internal\n * @packageDocumentation\n */\n\nimport type { Flame } from '../proto/animation';\nimport { decodeAnimationKeyframes } from '../proto/animation';\nimport { logger } from '../utils';\n\n/**\n * Interface for decoding protobuf animation data.\n * Allows custom decoder implementation if needed.\n * @internal\n */\nexport type AnimationDecoder = (protobufData: ArrayBuffer) => Flame[] | null;\n\n/**\n * Interface that applications must implement to render avatar frames.\n * The SDK calls these methods to control the avatar.\n * @internal\n */\nexport interface AvatarRenderer {\n /**\n * Render a single animation frame.\n * @param flame - The frame to render, or undefined to render idle\n * @param startIdle - If true and flame is undefined, start idle animation loop\n */\n renderFrame(flame: Flame | undefined, startIdle?: boolean): void;\n\n /**\n * Generate transition frames from idle position to target frame.\n * @param targetFrame - The target frame to transition to\n * @param frameCount - Number of transition frames to generate\n * @returns Promise resolving to array of transition frames\n */\n generateTransitionFromIdle(\n targetFrame: Flame,\n frameCount: number\n ): Promise<Flame[]>;\n\n /**\n * Check if the renderer is available and ready.\n * @returns true if renderer is ready to accept frames\n */\n isReady(): boolean;\n}\n\n/**\n * Configuration for AnimationHandler.\n * @internal\n */\nexport interface AnimationHandlerConfig {\n /**\n * Number of transition frames when starting animation.\n * Default: 8 (~320ms at 25fps)\n */\n transitionStartFrameCount?: number;\n\n /**\n * Number of transition frames when ending animation.\n * Default: 12 (~480ms at 25fps)\n */\n transitionEndFrameCount?: number;\n\n /**\n * Custom decoder function.\n * Uses built-in protobuf decoder if not provided.\n */\n decoder?: AnimationDecoder;\n\n /**\n * Callback when data stream stalls and fallback to idle is triggered.\n * Called when no animation frames received for 3 seconds during speaking session.\n */\n onStreamStalled?: () => void;\n\n /**\n * Enable jitter buffer for smoother 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 * Also controls how long to wait for a missing frame before skipping ahead.\n * Default: 80 (2 frames at 25fps)\n */\n maxBufferDelayMs?: number;\n}\n\n/**\n * Jitter buffer state machine.\n * @internal\n */\ntype BufferState = 'direct' | 'filling' | 'draining' | 'starved';\n\n/**\n * A frame held in the jitter buffer awaiting playback.\n * @internal\n */\ninterface BufferedFrame {\n flame: Flame;\n seq: number;\n receivedAt: number;\n}\n\n/**\n * AnimationHandler manages all animation playback and transition logic.\n *\n * @internal\n * This class is used internally by AvatarPlayer.\n * Applications should not instantiate this class directly.\n */\nexport class AnimationHandler {\n /** @internal */\n private renderer: AvatarRenderer;\n /** @internal */\n private decoder: AnimationDecoder;\n /** @internal */\n private config: Required<Omit<AnimationHandlerConfig, 'decoder' | 'onStreamStalled'>>;\n\n // Frame tracking\n /** @internal */\n private animationFrameCount = 0;\n /** @internal */\n private lastRenderedFrameSeq = -1;\n /** @internal */\n private renderedFrameCount = 0;\n\n // Transition state\n /** @internal */\n private isPlayingTransition = false;\n /** @internal */\n private isTransitioningToIdle = false;\n /** @internal */\n private transitionFrames: Flame[] = [];\n /** @internal */\n private transitionFrameIndex = 0;\n /** @internal */\n private transitionTimeoutId: ReturnType<typeof setTimeout> | null = null;\n\n // Guards against race conditions during async transition generation\n /** @internal */\n private isGeneratingStartTransition = false;\n /** @internal */\n private isGeneratingEndTransition = false;\n\n // Session-level flags to prevent duplicate handling\n // These are reset in resetTracking() for new sessions\n /** @internal */\n private hasHandledTransitionStart = false;\n /** @internal */\n private hasHandledTransitionEnd = false;\n\n // Watchdog for detecting stalled data stream\n /** @internal */\n private lastFrameReceivedTime = 0;\n /** @internal */\n private isInSession = false;\n /** @internal */\n private watchdogTimer: ReturnType<typeof setInterval> | null = null;\n /** @internal */\n private hasReportedStall = false;\n /** @internal */\n private static readonly STALL_TIMEOUT_MS = 3000;\n /** @internal */\n private onStreamStalledCallback: (() => void) | null = null;\n /** @internal */\n private isStalledFallback = false; // True when fallback to idle was triggered due to stall\n\n // Playback stats\n /** @internal */\n private playbackStatsTimer: ReturnType<typeof setInterval> | null = null;\n /** @internal */\n private playbackFrameCount = 0;\n /** @internal */\n private playbackFrameTimestamps: number[] = [];\n /** @internal */\n private playbackGapCount = 0;\n /** @internal */\n private playbackExpectedSeq = -1;\n\n // Jitter buffer\n /** @internal */\n private bufferState: BufferState = 'direct';\n /** @internal */\n private frameBuffer = new Map<number, BufferedFrame>();\n /** @internal */\n private bufferNextSeq = -1;\n /** @internal */\n private bufferDrainTimer: ReturnType<typeof setTimeout> | null = null;\n /** @internal */\n private bufferLastDrainTime = 0;\n /** @internal */\n private static readonly BUFFER_MAX_SIZE = 4;\n /** @internal */\n private static readonly BUFFER_INITIAL_FILL = 2;\n /** @internal */\n private static readonly BUFFER_FRAME_INTERVAL_MS = 40;\n\n /**\n * @internal\n */\n constructor(renderer: AvatarRenderer, config: AnimationHandlerConfig = {}) {\n this.renderer = renderer;\n this.decoder = config.decoder ?? decodeAnimationKeyframes;\n this.config = {\n transitionStartFrameCount: config.transitionStartFrameCount ?? 8,\n transitionEndFrameCount: config.transitionEndFrameCount ?? 12,\n enableJitterBuffer: config.enableJitterBuffer ?? true,\n maxBufferDelayMs: config.maxBufferDelayMs ?? 80,\n };\n this.onStreamStalledCallback = config.onStreamStalled ?? null;\n }\n\n /**\n * Handle animation data received from RTC provider.\n * @param protobufData - Raw protobuf bytes\n * @param frameSeq - Frame sequence number (optional)\n * @param isRecovered - Whether this frame was recovered via ALR\n * @internal\n */\n handleAnimationData(\n protobufData: ArrayBuffer,\n frameSeq?: number,\n isRecovered?: boolean\n ): void {\n // If we were in transition, stop it and play normal animation\n if (this.isPlayingTransition) {\n this.stopTransition();\n }\n\n // Update watchdog state\n const now = Date.now();\n if (this.hasReportedStall) {\n // Recovered from stall\n const stallDuration = now - this.lastFrameReceivedTime;\n logger.info('AnimationHandler', `Data stream resumed after ${stallDuration}ms stall`);\n this.hasReportedStall = false;\n }\n // If we were in stalled fallback, clear the flag (data has resumed, no transition needed)\n if (this.isStalledFallback) {\n logger.info('AnimationHandler', 'Resuming from stall fallback, rendering directly without transition');\n this.isStalledFallback = false;\n }\n this.lastFrameReceivedTime = now;\n\n this.animationFrameCount++;\n\n // Decode\n const keyframes = this.decoder(protobufData);\n if (!keyframes || keyframes.length === 0) {\n return;\n }\n\n // In lossy networks the transition packet may be delayed/lost.\n // Ensure session-level watchdog/stats still start on first valid playback frame.\n this.ensureSessionActive(frameSeq);\n\n // Jitter buffer path: insert into buffer instead of rendering directly\n if (this.config.enableJitterBuffer && frameSeq !== undefined) {\n this.bufferFrame(keyframes[0], frameSeq);\n return;\n }\n\n // Direct path (no buffer)\n // Check for out-of-order delivery\n if (frameSeq !== undefined && this.lastRenderedFrameSeq !== -1) {\n if (frameSeq < this.lastRenderedFrameSeq) {\n // Out-of-order frame - discard (already rendered a later frame)\n logger.warn(\n 'AnimationHandler',\n `OUT-OF-ORDER: seq=${frameSeq}, lastRendered=${this.lastRenderedFrameSeq}${isRecovered ? ' [RECOVERED]' : ''}, discarding`\n );\n return;\n } else if (frameSeq === this.lastRenderedFrameSeq) {\n // Duplicate frame - skip silently (expected when server sends redundant frames)\n return;\n } else if (frameSeq > this.lastRenderedFrameSeq + 1) {\n const gap = frameSeq - this.lastRenderedFrameSeq - 1;\n logger.warn(\n 'AnimationHandler',\n `GAP: ${gap} frame(s) between ${this.lastRenderedFrameSeq} and ${frameSeq}${isRecovered ? ' [RECOVERED]' : ''}`\n );\n }\n }\n\n if (frameSeq !== undefined) {\n this.lastRenderedFrameSeq = frameSeq;\n }\n\n // Render directly\n this.renderedFrameCount++;\n this.renderer.renderFrame(keyframes[0]);\n this.logRenderedFrame('direct', frameSeq, isRecovered);\n\n // Collect playback stats\n this.playbackFrameTimestamps.push(performance.now());\n this.playbackFrameCount++;\n if (frameSeq !== undefined) {\n if (this.playbackExpectedSeq >= 0 && frameSeq > this.playbackExpectedSeq) {\n this.playbackGapCount += frameSeq - this.playbackExpectedSeq;\n }\n this.playbackExpectedSeq = frameSeq + 1;\n }\n }\n\n /**\n * Handle transition packet - generate and play transition from idle to target.\n * Only starts transition on the first packet; subsequent packets are ignored while transitioning.\n * @param protobufData - Protobuf data containing target frame\n * @param frameCount - Number of transition frames (overrides config if provided)\n * @internal\n */\n async handleTransitionData(\n protobufData: ArrayBuffer,\n frameCount?: number\n ): Promise<void> {\n logger.info(\n 'AnimationHandler',\n `Start transition packet received (bytes=${protobufData.byteLength}, requestedFrames=${frameCount ?? this.config.transitionStartFrameCount}, hasHandledStart=${this.hasHandledTransitionStart}, isInSession=${this.isInSession}, isPlayingTransition=${this.isPlayingTransition}, isGeneratingStart=${this.isGeneratingStartTransition}, lastRenderedSeq=${this.lastRenderedFrameSeq}, bufferState=${this.bufferState}, buffered=${this.frameBuffer.size})`\n );\n\n // Ignore if we've already handled a transition start for this session\n if (this.hasHandledTransitionStart) {\n return;\n }\n // Ignore if already playing or generating a start transition\n if (this.isPlayingTransition && !this.isTransitioningToIdle) {\n return;\n }\n if (this.isGeneratingStartTransition) {\n return;\n }\n\n if (!this.renderer.isReady()) {\n logger.warn('AnimationHandler', 'Renderer not ready for transition');\n return;\n }\n\n // Once streaming playback has started, start-transition packets are stale.\n // Ignore them to avoid jumping back into transition and causing visual jitter.\n if (\n this.isInSession && (\n this.lastRenderedFrameSeq >= 0 ||\n this.frameBuffer.size > 0 ||\n this.bufferState !== 'direct'\n )\n ) {\n this.hasHandledTransitionStart = true;\n logger.warn(\n 'AnimationHandler',\n `Ignoring late transition packet after playback start (lastRenderedSeq=${this.lastRenderedFrameSeq}, bufferState=${this.bufferState}, buffered=${this.frameBuffer.size})`\n );\n return;\n }\n\n // Decode target frame\n const keyframes = this.decoder(protobufData);\n if (!keyframes || keyframes.length === 0) {\n logger.warn('AnimationHandler', 'No target keyframe in transition data');\n return;\n }\n\n // Mark that we've handled transition start for this session\n this.hasHandledTransitionStart = true;\n // Reset transition end flag since we're starting a new transition\n this.hasHandledTransitionEnd = false;\n\n // Start session and watchdog\n this.ensureSessionActive();\n\n const targetFrame = keyframes[0];\n const frames = frameCount ?? this.config.transitionStartFrameCount;\n logger.info('AnimationHandler', `Generating ${frames} transition frames to target`);\n\n // Set guard flag before async operation\n this.isGeneratingStartTransition = true;\n\n try {\n // Generate transition frames from idle to target\n const transitionFrames = await this.renderer.generateTransitionFromIdle(\n targetFrame,\n frames\n );\n logger.info('AnimationHandler', `Generated ${transitionFrames.length} transition frames`);\n\n // Start playing transition frames at 25fps\n this.isPlayingTransition = true;\n this.isTransitioningToIdle = false;\n this.transitionFrames = transitionFrames;\n this.transitionFrameIndex = 0;\n\n this.playTransitionFrame();\n } catch (error) {\n logger.error('AnimationHandler', 'Failed to generate transition:', error);\n // Fallback: render target directly\n this.renderer.renderFrame(targetFrame);\n this.logRenderedFrame('transition-fallback');\n } finally {\n this.isGeneratingStartTransition = false;\n }\n }\n\n /**\n * Handle transition end - generate and play reverse transition back to idle.\n * Only starts transition on the first packet; subsequent packets are ignored while transitioning.\n * @param protobufData - Protobuf data containing last animation frame\n * @param frameCount - Number of transition frames (overrides config if provided)\n * @internal\n */\n async handleTransitionToIdle(\n protobufData: ArrayBuffer,\n frameCount?: number\n ): Promise<void> {\n // Ignore stale transition-end packets after session has already returned to idle.\n // This can happen when duplicate packets arrive late over jittery networks.\n if (!this.isInSession) {\n logger.info('AnimationHandler', 'Ignoring transition end packet with no active session');\n return;\n }\n\n // Ignore if we've already handled a transition end for this session\n if (this.hasHandledTransitionEnd) {\n return;\n }\n // Ignore if already playing or generating an end transition\n if (this.isPlayingTransition && this.isTransitioningToIdle) {\n return;\n }\n if (this.isGeneratingEndTransition) {\n return;\n }\n\n if (!this.renderer.isReady()) {\n logger.warn('AnimationHandler', 'Renderer not ready for transition to idle');\n this.renderer.renderFrame(undefined, true);\n this.logRenderedFrame('idle');\n return;\n }\n\n // Decode last animation frame\n const keyframes = this.decoder(protobufData);\n if (!keyframes || keyframes.length === 0) {\n logger.warn('AnimationHandler', 'No last keyframe in transition end data, starting idle directly');\n this.renderer.renderFrame(undefined, true);\n this.logRenderedFrame('idle');\n return;\n }\n\n // Mark that we've handled transition end for this session\n this.hasHandledTransitionEnd = true;\n\n // Drop any queued speaking frames so they cannot interleave with end transition playback.\n this.flushBuffer();\n\n const lastFrame = keyframes[0];\n const frames = frameCount ?? this.config.transitionEndFrameCount;\n logger.info('AnimationHandler', `Generating ${frames} reverse transition frames to idle`);\n\n // Set guard flag before async operation\n this.isGeneratingEndTransition = true;\n\n try {\n // Generate transition frames from idle to last frame, then reverse\n const transitionFrames = await this.renderer.generateTransitionFromIdle(\n lastFrame,\n frames\n );\n logger.info('AnimationHandler', `Generated ${transitionFrames.length} transition frames, reversing for playback`);\n\n // Reverse frames to play from last animation frame back to idle\n const reversedFrames = transitionFrames.slice().reverse();\n\n // Start playing reversed transition at 25fps\n this.isPlayingTransition = true;\n this.isTransitioningToIdle = true;\n this.transitionFrames = reversedFrames;\n this.transitionFrameIndex = 0;\n\n this.playTransitionFrame();\n } catch (error) {\n logger.error('AnimationHandler', 'Failed to generate reverse transition:', error);\n this.renderer.renderFrame(undefined, true);\n this.logRenderedFrame('idle');\n } finally {\n this.isGeneratingEndTransition = false;\n }\n }\n\n /**\n * Start idle animation.\n * @internal\n */\n startIdle(): void {\n this.isInSession = false;\n this.hasReportedStall = false;\n this.renderer.renderFrame(undefined, true);\n this.logRenderedFrame('idle');\n }\n\n /**\n * Ensure session-level timers/stats are active.\n * @internal\n */\n private ensureSessionActive(frameSeq?: number): void {\n if (this.isInSession) {\n return;\n }\n\n this.isInSession = true;\n this.lastFrameReceivedTime = Date.now();\n this.hasReportedStall = false;\n this.startWatchdog();\n this.startPlaybackStats();\n\n if (frameSeq !== undefined) {\n logger.info('AnimationHandler', `Session started from animation frame seq=${frameSeq}`);\n }\n }\n\n /**\n * Reset animation frame tracking (call on session start).\n * @internal\n */\n resetTracking(): void {\n this.lastRenderedFrameSeq = -1;\n this.renderedFrameCount = 0;\n this.animationFrameCount = 0;\n // Reset session-level transition flags for new session\n this.hasHandledTransitionStart = false;\n this.hasHandledTransitionEnd = false;\n this.resetPlaybackStats();\n this.flushBuffer();\n logger.info('AnimationHandler', 'Frame tracking reset');\n }\n\n /**\n * Check if currently playing transition frames.\n * @internal\n */\n isInTransition(): boolean {\n return (\n this.isPlayingTransition\n || this.isGeneratingStartTransition\n || this.isGeneratingEndTransition\n );\n }\n\n /**\n * Stop transition playback.\n * @internal\n */\n stopTransition(): void {\n if (\n this.isPlayingTransition ||\n this.isGeneratingStartTransition ||\n this.isGeneratingEndTransition\n ) {\n logger.info('AnimationHandler', 'Stopping transition playback');\n }\n this.isPlayingTransition = false;\n this.isTransitioningToIdle = false;\n this.isGeneratingStartTransition = false;\n this.isGeneratingEndTransition = false;\n // Note: We intentionally do NOT reset hasHandledTransitionStart/End here\n // Those are session-level flags, only reset in resetTracking()\n this.transitionFrames = [];\n this.transitionFrameIndex = 0;\n if (this.transitionTimeoutId !== null) {\n clearTimeout(this.transitionTimeoutId);\n this.transitionTimeoutId = null;\n }\n this.flushBuffer();\n }\n\n /**\n * Clean up resources.\n * @internal\n */\n dispose(): void {\n this.stopTransition();\n this.stopWatchdog();\n }\n\n /**\n * Start the watchdog timer to detect stalled data streams.\n * @internal\n */\n private startWatchdog(): void {\n if (this.watchdogTimer) {\n return; // Already running\n }\n\n this.watchdogTimer = setInterval(() => {\n if (!this.isInSession) {\n return;\n }\n\n // Don't check during transition playback (we're playing generated frames, not receiving)\n if (this.isPlayingTransition) {\n return;\n }\n\n const elapsed = Date.now() - this.lastFrameReceivedTime;\n if (elapsed > AnimationHandler.STALL_TIMEOUT_MS && !this.hasReportedStall) {\n logger.error(\n 'AnimationHandler',\n `Data stream stalled: no frames received for ${elapsed}ms, falling back to idle`\n );\n this.hasReportedStall = true;\n this.isStalledFallback = true;\n\n // Trigger fallback to idle\n this.startIdle();\n\n // Notify external listener\n if (this.onStreamStalledCallback) {\n try {\n this.onStreamStalledCallback();\n } catch (e) {\n logger.error('AnimationHandler', 'Error in onStreamStalled callback:', e);\n }\n }\n }\n }, 1000); // Check every 1 second\n }\n\n /**\n * Stop the watchdog timer.\n * @internal\n */\n private stopWatchdog(): void {\n if (this.watchdogTimer) {\n clearInterval(this.watchdogTimer);\n this.watchdogTimer = null;\n }\n this.hasReportedStall = false;\n this.isStalledFallback = false;\n this.stopPlaybackStats();\n }\n\n /**\n * Start the playback stats reporting timer.\n * @internal\n */\n private startPlaybackStats(): void {\n if (this.playbackStatsTimer) {\n return; // Already running\n }\n this.resetPlaybackStats();\n this.playbackStatsTimer = setInterval(() => {\n this.reportPlaybackStats();\n }, 1000);\n }\n\n /**\n * Stop the playback stats reporting timer.\n * @internal\n */\n private stopPlaybackStats(): void {\n if (this.playbackStatsTimer) {\n clearInterval(this.playbackStatsTimer);\n this.playbackStatsTimer = null;\n }\n this.resetPlaybackStats();\n }\n\n /**\n * Reset playback stats counters.\n * @internal\n */\n private resetPlaybackStats(): void {\n this.playbackFrameCount = 0;\n this.playbackFrameTimestamps = [];\n this.playbackGapCount = 0;\n this.playbackExpectedSeq = -1;\n }\n\n /**\n * Report playback stats (called every 1s by timer).\n * Logs FPS, frame loss rate, and playback jitter.\n * @internal\n */\n private reportPlaybackStats(): void {\n // Skip reporting during transitions (locally generated frames)\n if (this.isPlayingTransition) {\n this.resetPlaybackStats();\n return;\n }\n\n // Skip if no frames were rendered this interval\n if (this.playbackFrameCount === 0) {\n return;\n }\n\n const fps = this.playbackFrameCount;\n const totalExpected = this.playbackFrameCount + this.playbackGapCount;\n const lossRate = totalExpected > 0 ? (this.playbackGapCount / totalExpected) * 100 : 0;\n\n // Calculate jitter: std deviation of inter-frame intervals\n let jitter = 0;\n if (this.playbackFrameTimestamps.length >= 2) {\n const intervals: number[] = [];\n for (let i = 1; i < this.playbackFrameTimestamps.length; i++) {\n intervals.push(this.playbackFrameTimestamps[i] - this.playbackFrameTimestamps[i - 1]);\n }\n const mean = intervals.reduce((a, b) => a + b, 0) / intervals.length;\n const variance = intervals.reduce((sum, v) => sum + (v - mean) ** 2, 0) / intervals.length;\n jitter = Math.sqrt(variance);\n }\n\n logger.info(\n 'AnimationHandler',\n `Playback stats: fps=${fps}, lossRate=${lossRate.toFixed(1)}%, jitter=${jitter.toFixed(1)}ms`\n );\n\n // Reset counters for next interval\n this.playbackFrameCount = 0;\n this.playbackFrameTimestamps = [];\n this.playbackGapCount = 0;\n }\n\n // ── Jitter Buffer ──\n\n /**\n * Insert a decoded frame into the jitter buffer.\n * Handles dedup, enforces max size, and drives the buffer state machine.\n * @internal\n */\n private bufferFrame(flame: Flame, seq: number): void {\n // Never allow frames older than what we've already rendered.\n if (this.lastRenderedFrameSeq >= 0 && seq <= this.lastRenderedFrameSeq) {\n logger.warn(\n 'AnimationHandler',\n `Jitter buffer: dropping stale frame seq=${seq} (lastRendered=${this.lastRenderedFrameSeq})`\n );\n return;\n }\n\n // If we're already waiting for a newer sequence, this frame arrived too late.\n if (this.bufferNextSeq >= 0 && seq < this.bufferNextSeq) {\n logger.warn(\n 'AnimationHandler',\n `Jitter buffer: dropping late frame seq=${seq} (nextExpected=${this.bufferNextSeq})`\n );\n return;\n }\n\n // Dedup — server sends each frame twice for resilience\n if (this.frameBuffer.has(seq)) {\n return;\n }\n\n this.frameBuffer.set(seq, { flame, seq, receivedAt: performance.now() });\n\n // Enforce max buffer size — drop oldest when full\n if (this.frameBuffer.size > AnimationHandler.BUFFER_MAX_SIZE) {\n let oldestSeq = Infinity;\n for (const k of this.frameBuffer.keys()) {\n if (k < oldestSeq) oldestSeq = k;\n }\n this.frameBuffer.delete(oldestSeq);\n logger.warn(\n 'AnimationHandler',\n `Jitter buffer: overflow, dropping seq=${oldestSeq} (nextExpected=${this.bufferNextSeq})`\n );\n }\n\n // State machine transitions\n switch (this.bufferState) {\n case 'direct':\n this.bufferState = 'filling';\n if (this.bufferNextSeq < 0) {\n this.bufferNextSeq = seq;\n }\n logger.info('AnimationHandler', `Jitter buffer: filling (first frame seq=${seq})`);\n if (this.frameBuffer.size >= AnimationHandler.BUFFER_INITIAL_FILL) {\n this.startBufferDrain();\n }\n break;\n case 'filling':\n if (this.frameBuffer.size >= AnimationHandler.BUFFER_INITIAL_FILL) {\n this.startBufferDrain();\n }\n break;\n case 'starved':\n this.startBufferDrain();\n break;\n case 'draining':\n // Already draining, frame will be picked up by drain loop\n break;\n }\n }\n\n /**\n * Drop buffered frames that are now too old to ever be rendered in-order.\n * @internal\n */\n private dropStaleBufferedFrames(): void {\n if (this.frameBuffer.size === 0) {\n return;\n }\n\n const minAllowedSeq = Math.max(this.bufferNextSeq, this.lastRenderedFrameSeq + 1);\n if (minAllowedSeq < 0) {\n return;\n }\n\n let dropped = 0;\n for (const seq of Array.from(this.frameBuffer.keys())) {\n if (seq < minAllowedSeq) {\n this.frameBuffer.delete(seq);\n dropped++;\n }\n }\n\n if (dropped > 0) {\n logger.warn(\n 'AnimationHandler',\n `Jitter buffer: dropped ${dropped} stale frame(s) older than seq=${minAllowedSeq}`\n );\n }\n }\n\n /**\n * Find the lowest buffered sequence at or after minSeq.\n * @internal\n */\n private findLowestBufferedSeqAtOrAfter(minSeq: number): number | null {\n let candidate = Infinity;\n for (const seq of this.frameBuffer.keys()) {\n if (seq >= minSeq && seq < candidate) {\n candidate = seq;\n }\n }\n return candidate === Infinity ? null : candidate;\n }\n\n /**\n * Begin draining the buffer at 25fps.\n * @internal\n */\n private startBufferDrain(): void {\n this.bufferState = 'draining';\n if (this.bufferNextSeq < 0) {\n let minSeq = Infinity;\n for (const k of this.frameBuffer.keys()) {\n if (k < minSeq) minSeq = k;\n }\n this.bufferNextSeq = minSeq;\n }\n logger.info('AnimationHandler', `Jitter buffer: draining (${this.frameBuffer.size} frames buffered)`);\n this.bufferLastDrainTime = performance.now();\n this.drainBufferFrame();\n }\n\n /**\n * Drain one frame from the buffer and schedule the next drain.\n * Handles missing frames (skip-ahead after maxBufferDelayMs) and starvation.\n * @internal\n */\n private drainBufferFrame(): void {\n this.bufferDrainTimer = null;\n\n if (this.bufferState !== 'draining') {\n return;\n }\n\n this.dropStaleBufferedFrames();\n\n const frame = this.frameBuffer.get(this.bufferNextSeq);\n\n if (frame) {\n // Expected frame found — render it\n this.renderBufferedFrame(frame);\n this.frameBuffer.delete(this.bufferNextSeq);\n this.bufferNextSeq++;\n } else if (this.frameBuffer.size > 0) {\n // Expected frame missing — check if we should skip ahead to the next in-order frame.\n const nextSeq = this.findLowestBufferedSeqAtOrAfter(this.bufferNextSeq);\n\n if (nextSeq === null) {\n this.bufferState = 'starved';\n logger.warn('AnimationHandler', 'Jitter buffer: no in-order frames available, pausing drain');\n return;\n }\n\n const nextFrame = this.frameBuffer.get(nextSeq)!;\n const waitTime = performance.now() - nextFrame.receivedAt;\n\n if (waitTime > this.config.maxBufferDelayMs) {\n // Missing frame didn't arrive in time — skip ahead to the next available sequence.\n const gap = Math.max(0, nextSeq - this.bufferNextSeq);\n this.playbackGapCount += gap;\n logger.warn(\n 'AnimationHandler',\n `Jitter buffer: skipping ${gap} frame(s) from seq ${this.bufferNextSeq} to ${nextSeq} after ${waitTime.toFixed(1)}ms`\n );\n this.renderBufferedFrame(nextFrame);\n this.frameBuffer.delete(nextSeq);\n this.bufferNextSeq = nextSeq + 1;\n }\n // else: wait for the missing frame (render nothing this cycle)\n } else {\n // Buffer empty — starvation\n this.bufferState = 'starved';\n logger.warn('AnimationHandler', 'Jitter buffer: starved, pausing drain');\n return; // Don't schedule next drain\n }\n\n // Schedule next drain with drift correction\n const now = performance.now();\n const nextTarget = this.bufferLastDrainTime + AnimationHandler.BUFFER_FRAME_INTERVAL_MS;\n const delay = Math.max(0, nextTarget - now);\n this.bufferLastDrainTime = nextTarget;\n this.bufferDrainTimer = setTimeout(() => this.drainBufferFrame(), delay);\n }\n\n /**\n * Render a single frame from the buffer and collect playback stats.\n * @internal\n */\n private renderBufferedFrame(frame: BufferedFrame): void {\n if (this.lastRenderedFrameSeq >= 0 && frame.seq <= this.lastRenderedFrameSeq) {\n logger.warn(\n 'AnimationHandler',\n `Jitter buffer: refusing out-of-order render seq=${frame.seq} (lastRendered=${this.lastRenderedFrameSeq})`\n );\n return;\n }\n\n this.renderer.renderFrame(frame.flame);\n this.lastRenderedFrameSeq = frame.seq;\n this.renderedFrameCount++;\n this.logRenderedFrame('buffer', frame.seq);\n\n // Playback stats\n this.playbackFrameTimestamps.push(performance.now());\n this.playbackFrameCount++;\n }\n\n /**\n * Flush the jitter buffer, stop the drain loop, and revert to direct mode.\n * @internal\n */\n private flushBuffer(): void {\n this.frameBuffer.clear();\n this.bufferState = 'direct';\n this.bufferNextSeq = -1;\n this.bufferLastDrainTime = 0;\n if (this.bufferDrainTimer !== null) {\n clearTimeout(this.bufferDrainTimer);\n this.bufferDrainTimer = null;\n }\n }\n\n /**\n * Play a single transition frame and schedule the next one.\n * @internal\n */\n private playTransitionFrame(): void {\n if (\n !this.isPlayingTransition ||\n this.transitionFrameIndex >= this.transitionFrames.length\n ) {\n // Transition complete\n const wasTransitioningToIdle = this.isTransitioningToIdle;\n this.isPlayingTransition = false;\n this.isTransitioningToIdle = false;\n this.transitionFrames = [];\n this.transitionFrameIndex = 0;\n if (this.transitionTimeoutId !== null) {\n clearTimeout(this.transitionTimeoutId);\n this.transitionTimeoutId = null;\n }\n logger.info('AnimationHandler', 'Transition playback complete');\n\n // If transitioning to idle, start idle animation\n if (wasTransitioningToIdle) {\n logger.info('AnimationHandler', 'Starting idle animation after transition');\n this.renderer.renderFrame(undefined, true);\n this.logRenderedFrame('idle');\n }\n return;\n }\n\n if (!this.renderer.isReady()) {\n this.isPlayingTransition = false;\n this.isTransitioningToIdle = false;\n return;\n }\n\n const frame = this.transitionFrames[this.transitionFrameIndex];\n this.renderer.renderFrame(frame);\n this.logRenderedFrame('transition');\n this.transitionFrameIndex++;\n\n // Schedule next frame at 25fps (40ms)\n this.transitionTimeoutId = setTimeout(() => {\n if (this.isPlayingTransition) {\n this.playTransitionFrame();\n }\n }, 40);\n }\n\n /**\n * Emit a per-frame render log for debugging ordering issues.\n * @internal\n */\n private logRenderedFrame(\n source: 'direct' | 'buffer' | 'transition' | 'idle' | 'transition-fallback',\n seq?: number,\n isRecovered?: boolean\n ): void {\n logger.info(\n 'AnimationHandler',\n `Rendered frame: source=${source}, seq=${seq ?? 'n/a'}${isRecovered ? ' [RECOVERED]' : ''}`\n );\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"animation-worker.d.ts","sourceRoot":"","sources":["../../../src/providers/livekit/animation-worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;
|
|
1
|
+
{"version":3,"file":"animation-worker.d.ts","sourceRoot":"","sources":["../../../src/providers/livekit/animation-worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAqkBH,OAAO,EAAE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spatialwalk/avatarkit-rtc",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.0-beta.
|
|
4
|
+
"version": "1.0.0-beta.8",
|
|
5
5
|
"description": "Unified RTC adapter for avatarkit - supports LiveKit, Agora and other RTC providers",
|
|
6
6
|
"author": "SPAvatar Team",
|
|
7
7
|
"license": "MIT",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"@bufbuild/protobuf": "^2.10.2"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@spatialwalk/avatarkit": "^1.0.0-beta.
|
|
36
|
+
"@spatialwalk/avatarkit": "^1.0.0-beta.82",
|
|
37
37
|
"livekit-client": "2.16.1",
|
|
38
38
|
"agora-rtc-sdk-ng": "^4.x"
|
|
39
39
|
},
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
}
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@spatialwalk/avatarkit": "1.0.0-beta.
|
|
49
|
+
"@spatialwalk/avatarkit": "1.0.0-beta.82",
|
|
50
50
|
"@types/node": "^22.10.2",
|
|
51
51
|
"livekit-client": "2.16.1",
|
|
52
52
|
"agora-rtc-sdk-ng": "^4.21.0",
|
|
@@ -58,4 +58,4 @@
|
|
|
58
58
|
"jsdom": "^26.0.0",
|
|
59
59
|
"happy-dom": "^20.3.7"
|
|
60
60
|
}
|
|
61
|
-
}
|
|
61
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"animation-worker-CUXZycUw.js","sources":["../src/providers/livekit/animation-worker.ts"],"sourcesContent":["/**\n * RTCRtpScriptTransform Worker for Animation Track (VP8 Video)\n *\n * This worker handles extracting animation data from VP8 video frames.\n * The server prepends a minimal VP8 header to trick the browser's depacketizer.\n *\n * Frame format after depacketization (VP8 descriptor is stripped by depacketizer):\n * [0-9] VP8 frame header (10 bytes) - minimal keyframe structure, skipped\n * [10] flags (uint8) - packet flags\n * [11-14] msgLen (little-endian u32) - length of protobuf message\n * [15..15+msgLen) protobuf Message binary\n */\n\n// Type declarations for RTCRtpScriptTransform API (experimental)\ndeclare interface RTCTransformEvent extends Event {\n transformer: {\n readable: ReadableStream;\n writable: WritableStream;\n options?: unknown;\n };\n}\n\n// Worker global scope with RTCRtpScriptTransform support\ndeclare const self: DedicatedWorkerGlobalScope & {\n onrtctransform?: ((event: RTCTransformEvent) => void) | null;\n};\n\n// VP8 header size (frame tag + sync code + dimensions, descriptor already stripped)\nconst VP8_FRAME_HEADER_SIZE = 10;\n\n// Protocol constants (must match egress-server/metadata_track.go)\nconst METADATA_FIXED_HEADER_SIZE = 5; // 1B flags + 4B length\n\n// Packet flags\nconst PACKET_FLAG_IDLE = 0x01;\nconst PACKET_FLAG_START = 0x02;\nconst PACKET_FLAG_END = 0x04;\nconst PACKET_FLAG_GZIPPED = 0x08;\nconst PACKET_FLAG_TRANSITION = 0x10; // Transition packet - contains target frame for generating transition from idle\nconst PACKET_FLAG_TRANSITION_END = 0x20; // Transition end packet - contains last frame for generating transition back to idle\nconst PACKET_FLAG_REDUNDANT = 0x40; // Packet contains redundant previous frame for ALR loss recovery\n\ninterface FrameMetadata {\n flags: number;\n protobufLength: number;\n protobufData: Uint8Array;\n // ALR (Application-Level Redundancy) data\n hasRedundant: boolean;\n redundantLength: number;\n redundantData: Uint8Array | null;\n}\n\ninterface TransformOptions {\n operation: 'receiver';\n}\n\n// State for receiver\nlet receiverMetaCount = 0;\nlet lastLogTime = 0;\nlet totalFrames = 0;\nlet framesWithMeta = 0;\nlet wasIdle = false; // Track idle state for transition detection\n\n// ALR (Application-Level Redundancy) state for loss detection\nlet lastReceivedTimestamp: number | null = null;\nlet framesRecovered = 0;\nlet framesLost = 0;\n// Expected timestamp increment per animation frame (90000 Hz * 40ms = 3600)\n// Animation is sent every 2nd audio frame (40ms intervals)\nconst EXPECTED_TIMESTAMP_INCREMENT = 3600;\n\n// Sequence tracking for out-of-order detection\nlet lastRenderedSeq: number = -1; // Last sequence number sent to main thread\nlet framesOutOfOrder = 0; // Count of out-of-order frames detected\nlet framesDuplicate = 0; // Count of duplicate frames skipped\nlet framesDropped = 0; // Count of frames that were permanently lost (gaps in sequence)\nlet framesSent = 0; // Total frames successfully sent to main thread\n\nfunction parseMetadataHeader(data: Uint8Array): { meta: FrameMetadata; headerSize: number } | null {\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); // little-endian\n\n // Calculate minimum size needed\n const headerSize = METADATA_FIXED_HEADER_SIZE + msgLen;\n if (data.byteLength < headerSize) return null;\n\n // The compressed payload (will be decompressed later)\n const compressedData = data.slice(METADATA_FIXED_HEADER_SIZE, METADATA_FIXED_HEADER_SIZE + msgLen);\n\n // Note: With the new ALR optimization, both frames are gzipped together\n // The format INSIDE the gzip (after decompression) is:\n // - ALR: [currentLen (4B)][currentData][prevLen (4B)][prevData]\n // - Non-ALR: just the raw protobuf data\n // We return the compressed data here; decompression and ALR parsing happens in receiverTransform\n\n const hasRedundant = (flags & PACKET_FLAG_REDUNDANT) !== 0;\n\n return {\n meta: {\n flags,\n protobufLength: msgLen,\n protobufData: compressedData, // This is still compressed at this point\n hasRedundant,\n redundantLength: 0, // Will be determined after decompression\n redundantData: null, // Will be extracted after decompression\n },\n headerSize,\n };\n}\n\nfunction isIdlePacket(flags: number): boolean {\n return (flags & PACKET_FLAG_IDLE) !== 0;\n}\n\nfunction isStartPacket(flags: number): boolean {\n return (flags & PACKET_FLAG_START) !== 0;\n}\n\nfunction isEndPacket(flags: number): boolean {\n return (flags & PACKET_FLAG_END) !== 0;\n}\n\nfunction isGzipped(flags: number): boolean {\n return (flags & PACKET_FLAG_GZIPPED) !== 0;\n}\n\nfunction isTransitionPacket(flags: number): boolean {\n return (flags & PACKET_FLAG_TRANSITION) !== 0;\n}\n\nfunction isTransitionEndPacket(flags: number): boolean {\n return (flags & PACKET_FLAG_TRANSITION_END) !== 0;\n}\n\n// Decompress gzipped data using DecompressionStream API\nasync function decompressGzip(data: Uint8Array): Promise<Uint8Array> {\n const ds = new DecompressionStream('gzip');\n const writer = ds.writable.getWriter();\n // Create a copy to ensure we have a plain ArrayBuffer, not SharedArrayBuffer\n const copy = new Uint8Array(data);\n writer.write(copy);\n writer.close();\n\n const reader = ds.readable.getReader();\n const chunks: Uint8Array[] = [];\n let totalLength = 0;\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n totalLength += value.length;\n }\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\n return result;\n}\n\n// Compression stats for logging\nlet totalCompressedBytes = 0;\nlet totalUncompressedBytes = 0;\n\n// ALR payload structure after decompression\ninterface ALRPayload {\n frameSeq: number; // Sequence number of current frame\n currentData: Uint8Array; // Current frame (N)\n prev1Data: Uint8Array | null; // Previous frame (N-1)\n prev2Data: Uint8Array | null; // Frame before previous (N-2)\n}\n\n// Parse ALR payload after decompression\n// ALR format: [frameSeq (4B)][currentLen (4B)][currentData][prev1Len (4B)][prev1Data][prev2Len (4B)][prev2Data]\n// Non-ALR format: [frameSeq (4B)][raw protobuf data]\n// Returns { frameSeq, currentData, prev1Data, prev2Data } or null if parsing fails\nfunction parseALRPayload(decompressed: Uint8Array, hasRedundant: boolean): ALRPayload | null {\n if (decompressed.byteLength < 4) return null;\n\n const view = new DataView(decompressed.buffer, decompressed.byteOffset, decompressed.byteLength);\n let offset = 0;\n\n // Parse frame sequence number\n const frameSeq = view.getUint32(offset, true);\n offset += 4;\n\n if (!hasRedundant) {\n // Non-ALR: rest is raw protobuf\n const currentData = decompressed.slice(offset);\n return { frameSeq, currentData, prev1Data: null, prev2Data: null };\n }\n\n // ALR format: parse current frame\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\n // Parse prev1 (N-1) if available\n let prev1Data: Uint8Array | null = 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\n // Parse prev2 (N-2) if available\n let prev2Data: Uint8Array | null = 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\n return { frameSeq, currentData, prev1Data, prev2Data };\n}\n\n// Helper to send animation data to main thread with sequence tracking\n// Returns true if frame was sent, false if skipped (duplicate/out-of-order)\nfunction sendAnimationToMainThread(\n protobufData: Uint8Array,\n flags: number,\n frameSeq: number,\n isRecovered: boolean = false\n): boolean {\n const isIdle = isIdlePacket(flags);\n const isStart = isStartPacket(flags);\n const isEnd = isEndPacket(flags);\n\n // Check for out-of-order or duplicate frames\n if (frameSeq <= lastRenderedSeq && lastRenderedSeq !== -1 && !isStart) {\n if (frameSeq === lastRenderedSeq) {\n framesDuplicate++;\n } else {\n framesOutOfOrder++;\n }\n return false;\n }\n\n // Check for gap (frames we never received and couldn't recover) - these are DROPPED\n if (lastRenderedSeq !== -1 && frameSeq > lastRenderedSeq + 1 && !isStart) {\n const gap = frameSeq - lastRenderedSeq - 1;\n framesDropped += gap;\n }\n\n framesSent++;\n\n lastRenderedSeq = frameSeq;\n\n const protobufBuffer = new ArrayBuffer(protobufData.byteLength);\n new Uint8Array(protobufBuffer).set(protobufData);\n\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\n return true;\n}\n\n// Receiver transform: extract animation data from video frames\n// Uses ALR (Application-Level Redundancy) to recover lost frames\nfunction receiverTransform(frame: RTCEncodedVideoFrame, _controller: TransformStreamDefaultController<RTCEncodedVideoFrame>) {\n totalFrames++;\n const data = new Uint8Array(frame.data);\n const currentTimestamp = frame.timestamp;\n\n // Skip the VP8 frame header (10 bytes) - the VP8 descriptor is already stripped by depacketizer\n // Check if we have enough data for VP8 header + at least 1 byte of animation data\n if (data.length <= VP8_FRAME_HEADER_SIZE) {\n return; // Frame too small, skip\n }\n\n // Extract animation data after VP8 header\n const animData = data.subarray(VP8_FRAME_HEADER_SIZE);\n\n const parsed = parseMetadataHeader(animData);\n\n if (parsed) {\n const { meta } = parsed;\n framesWithMeta++;\n receiverMetaCount++;\n\n const isIdle = isIdlePacket(meta.flags);\n const isStart = isStartPacket(meta.flags);\n const isEnd = isEndPacket(meta.flags);\n\n // ALR: Detect frame loss and recover using redundant data\n // Check if we missed frames by looking at timestamp gap\n if (lastReceivedTimestamp !== null && !isIdle && !isStart) {\n const timestampDelta = currentTimestamp - lastReceivedTimestamp;\n // Allow some tolerance (1.5x expected increment) for timing variations\n if (timestampDelta > EXPECTED_TIMESTAMP_INCREMENT * 1.5) {\n const missedFrames = Math.round(timestampDelta / EXPECTED_TIMESTAMP_INCREMENT) - 1;\n framesLost += missedFrames;\n\n // Try to recover using redundant data from current packet\n // With ALR optimization, up to 3 frames are gzipped together\n if (meta.hasRedundant && isGzipped(meta.flags)) {\n // Decompress to get all frames, then recover\n totalCompressedBytes += meta.protobufData.byteLength;\n\n decompressGzip(meta.protobufData)\n .then((decompressed) => {\n totalUncompressedBytes += decompressed.byteLength;\n\n const parsed = parseALRPayload(decompressed, true);\n if (parsed) {\n const currentSeq = parsed.frameSeq;\n\n // Determine how many frames we can recover based on how many were lost\n // and how many redundant frames we have\n const framesToRecover: Array<{data: Uint8Array, seq: number}> = [];\n\n if (missedFrames >= 2 && parsed.prev2Data) {\n framesToRecover.push({ data: parsed.prev2Data, seq: currentSeq - 2 });\n }\n if (missedFrames >= 1 && parsed.prev1Data) {\n framesToRecover.push({ data: parsed.prev1Data, seq: currentSeq - 1 });\n }\n\n const recovered = framesToRecover.length;\n if (recovered > 0) {\n framesRecovered += recovered;\n\n // Send recovered frames in chronological order (oldest first)\n for (const frame of framesToRecover) {\n sendAnimationToMainThread(frame.data, meta.flags & ~PACKET_FLAG_REDUNDANT, frame.seq, true);\n }\n }\n\n // Then send current frame\n sendAnimationToMainThread(parsed.currentData, meta.flags & ~PACKET_FLAG_REDUNDANT, currentSeq, false);\n }\n })\n .catch((err) => {\n console.error(`[Animation Worker] ALR decompression error:`, err);\n });\n\n // Update timestamp and return early (we've handled this frame)\n lastReceivedTimestamp = currentTimestamp;\n return;\n }\n }\n }\n\n // Update last received timestamp (skip for idle packets to avoid false loss detection)\n if (!isIdle) {\n lastReceivedTimestamp = currentTimestamp;\n }\n\n // Reset state on session start\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\n const isTransition = isTransitionPacket(meta.flags);\n\n if (isIdle) {\n // First idle packet - post idleStart event\n if (!wasIdle) {\n self.postMessage({ type: 'idleStart' });\n wasIdle = true;\n }\n } else if (isTransition && meta.protobufLength > 0) {\n // Transition packet - contains target frame for smooth transition from idle\n wasIdle = false;\n\n // Decompress if gzipped (transition packets are always gzipped)\n const gzipped = isGzipped(meta.flags);\n if (gzipped) {\n totalCompressedBytes += meta.protobufData.byteLength;\n\n decompressGzip(meta.protobufData).then((decompressed) => {\n totalUncompressedBytes += decompressed.byteLength;\n\n const protobufBuffer = new ArrayBuffer(decompressed.byteLength);\n new Uint8Array(protobufBuffer).set(decompressed);\n\n // Post transition event - main thread will generate transition frames\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\n self.postMessage({\n type: 'transition',\n flags: meta.flags,\n protobufData: protobufBuffer,\n }, { transfer: [protobufBuffer] });\n }\n } else if (isTransitionEndPacket(meta.flags) && meta.protobufLength > 0) {\n // Transition end packet - contains last frame for smooth transition back to idle\n\n // Decompress if gzipped (transition end packets are always gzipped)\n const gzipped = isGzipped(meta.flags);\n if (gzipped) {\n totalCompressedBytes += meta.protobufData.byteLength;\n\n decompressGzip(meta.protobufData).then((decompressed) => {\n totalUncompressedBytes += decompressed.byteLength;\n\n const protobufBuffer = new ArrayBuffer(decompressed.byteLength);\n new Uint8Array(protobufBuffer).set(decompressed);\n\n // Post transition end event - main thread will generate reverse transition frames\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\n self.postMessage({\n type: 'transitionEnd',\n flags: meta.flags,\n protobufData: protobufBuffer,\n }, { transfer: [protobufBuffer] });\n }\n } else if (meta.protobufLength > 0) {\n // Normal animation packet\n // Transition from idle to streaming (if not already transitioned via transition packet)\n if (wasIdle) {\n wasIdle = false;\n }\n\n // Decompress if gzipped\n const gzipped = isGzipped(meta.flags);\n if (gzipped) {\n // Track compression stats\n totalCompressedBytes += meta.protobufData.byteLength;\n\n // Async decompression\n decompressGzip(meta.protobufData).then((decompressed) => {\n totalUncompressedBytes += decompressed.byteLength;\n\n // Parse ALR format to get frame sequence and data\n // Format: [frameSeq (4B)][currentLen (4B)][currentData]... or [frameSeq (4B)][raw protobuf]\n const parsed = parseALRPayload(decompressed, meta.hasRedundant);\n if (parsed) {\n sendAnimationToMainThread(parsed.currentData, meta.flags & ~PACKET_FLAG_REDUNDANT, parsed.frameSeq, false);\n }\n }).catch(() => {});\n } else {\n // Uncompressed - this shouldn't happen in normal operation but handle it\n const protobufBuffer = new ArrayBuffer(meta.protobufData.byteLength);\n new Uint8Array(protobufBuffer).set(meta.protobufData);\n\n self.postMessage({\n type: 'animation',\n flags: meta.flags,\n isIdle: false,\n isStart,\n isEnd,\n frameSeq: -1, // Unknown sequence\n protobufData: protobufBuffer,\n }, { transfer: [protobufBuffer] });\n }\n }\n\n // Report stats every second\n const now = performance.now();\n if (now - lastLogTime > 1000) {\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\n // For video, we don't enqueue the frame since it's not real video\n // The browser's VP8 decoder would fail on our custom data anyway\n // controller.enqueue(frame);\n}\n\n// Handle rtctransform event (RTCRtpScriptTransform)\nself.onrtctransform = (event: RTCTransformEvent) => {\n const transformer = event.transformer;\n const options = transformer.options as TransformOptions;\n\n try {\n if (options.operation === 'receiver') {\n // Reset state\n receiverMetaCount = 0;\n lastLogTime = 0;\n totalFrames = 0;\n framesWithMeta = 0;\n wasIdle = 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\n transformer.readable\n .pipeThrough(new TransformStream({ transform: receiverTransform }))\n .pipeTo(transformer.writable)\n .catch((err: unknown) => {\n console.error('[Animation Worker] Pipeline error:', err);\n self.postMessage({ type: 'error', 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({ type: 'error', error: `Animation transform setup error: ${err}` });\n }\n};\n\n// Handle message-based initialization for fallback\nself.onmessage = (event: MessageEvent) => {\n const { type } = event.data;\n if (type === 'init') {\n self.postMessage({ type: 'initialized' });\n }\n};\n\nexport {};\n"],"names":["currentData","parsed","frame"],"mappings":"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"}
|