@spatialwalk/avatarkit-rtc 1.0.0-beta.5 → 1.0.0-beta.7
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/core/AvatarPlayer.d.ts.map +1 -1
- package/dist/core/RTCProvider.d.ts +0 -10
- package/dist/core/RTCProvider.d.ts.map +1 -1
- package/dist/index10.js +64 -346
- package/dist/index10.js.map +1 -1
- package/dist/index11.js +14 -104
- package/dist/index11.js.map +1 -1
- package/dist/index12.js +386 -14
- package/dist/index12.js.map +1 -1
- package/dist/index2.js +32 -4
- package/dist/index2.js.map +1 -1
- package/dist/index3.js +131 -26
- package/dist/index3.js.map +1 -1
- package/dist/index4.js +1 -17
- package/dist/index4.js.map +1 -1
- package/dist/index6.js +128 -16
- package/dist/index6.js.map +1 -1
- package/dist/index9.js.map +1 -1
- package/dist/providers/agora/AgoraProvider.d.ts +0 -8
- package/dist/providers/agora/AgoraProvider.d.ts.map +1 -1
- package/dist/providers/base/BaseProvider.d.ts +0 -8
- package/dist/providers/base/BaseProvider.d.ts.map +1 -1
- package/dist/providers/livekit/LiveKitProvider.d.ts +0 -8
- package/dist/providers/livekit/LiveKitProvider.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/index4.js
CHANGED
|
@@ -3,7 +3,7 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en
|
|
|
3
3
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
4
|
import { BaseProvider } from "./index9.js";
|
|
5
5
|
import { isAgoraConfig, ConnectionState } from "./index5.js";
|
|
6
|
-
import { SEIExtractor } from "./
|
|
6
|
+
import { SEIExtractor } from "./index12.js";
|
|
7
7
|
import { logger } from "./index7.js";
|
|
8
8
|
class AgoraProvider extends BaseProvider {
|
|
9
9
|
constructor(_options = {}) {
|
|
@@ -342,22 +342,6 @@ class AgoraProvider extends BaseProvider {
|
|
|
342
342
|
this.localAudioTrack.close();
|
|
343
343
|
this.localAudioTrack = null;
|
|
344
344
|
}
|
|
345
|
-
/**
|
|
346
|
-
* Play remote audio (resume playback)
|
|
347
|
-
*/
|
|
348
|
-
playRemoteAudio() {
|
|
349
|
-
this.remoteAudioTracks.forEach((track) => {
|
|
350
|
-
track.play();
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
/**
|
|
354
|
-
* Pause remote audio
|
|
355
|
-
*/
|
|
356
|
-
pauseRemoteAudio() {
|
|
357
|
-
this.remoteAudioTracks.forEach((track) => {
|
|
358
|
-
track.stop();
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
345
|
/**
|
|
362
346
|
* Get the native Agora RTC Client instance.
|
|
363
347
|
*
|
package/dist/index4.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index4.js","sources":["../src/providers/agora/AgoraProvider.ts"],"sourcesContent":["/**\n * Agora Provider Implementation.\n *\n * This provider uses Agora's H.264 SEI approach\n * to transport animation data.\n *\n * Key differences from LiveKit:\n * - Uses native SEI events instead of RTCRtpScriptTransform\n * - No ALR (Application-Level Redundancy) needed - Agora handles reliability\n * - Simpler data extraction - uses SEI header parsing\n *\n * @packageDocumentation\n */\n\nimport { BaseProvider } from '../base/BaseProvider';\nimport type { RTCConnectionConfig, AgoraConnectionConfig } from '../../types';\nimport { isAgoraConfig, ConnectionState } from '../../types';\nimport type { AnimationTrackCallbacks, AudioTrackCallbacks } from '../../core/types';\nimport { SEIExtractor } from './SEIExtractor';\nimport { logger } from '../../utils';\nimport type {\n AgoraClient,\n AgoraLocalAudioTrack,\n AgoraRemoteAudioTrack,\n AgoraRemoteVideoTrack,\n AgoraUID,\n} from './types';\n\n/**\n * Agora Provider options.\n * @internal Reserved for future use\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-interface\nexport interface AgoraProviderOptions {\n // Reserved for future configuration options\n}\n\n/**\n * Remote user info with tracks\n * @internal\n */\ninterface RemoteUserInfo {\n videoTrack?: AgoraRemoteVideoTrack;\n audioTrack?: AgoraRemoteAudioTrack;\n}\n\n/**\n * Agora Provider.\n *\n * Implements RTCProvider interface for Agora platform.\n * Uses native SEI events to receive animation data from H.264 video tracks.\n *\n * @example\n * ```typescript\n * import { AvatarPlayer } from '@spatialwalk/avatarkit-rtc';\n * import { AgoraProvider } from '@spatialwalk/avatarkit-rtc/providers/agora';\n *\n * const provider = new AgoraProvider();\n * const player = new AvatarPlayer(provider, renderer);\n *\n * await player.connect({\n * appId: 'your-agora-app-id',\n * channel: 'your-channel',\n * token: 'your-token',\n * });\n * ```\n */\nexport class AgoraProvider extends BaseProvider {\n /** Provider name identifier */\n readonly name = 'agora';\n\n /** @internal */\n private client: AgoraClient | null = null;\n /** @internal Dynamic SDK - type is any due to dynamic import */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private agoraSDK: any = null;\n\n // Animation track subscription (stored for cleanup, actual callbacks handled by SEIExtractor)\n /** @internal */\n private _animationCallbacks: AnimationTrackCallbacks | null = null;\n /** @internal */\n private seiExtractor: SEIExtractor | null = null;\n /** @internal */\n private remoteUsers: Map<AgoraUID, RemoteUserInfo> = new Map();\n /** @internal */\n private videoContainers: Map<AgoraUID, HTMLDivElement> = new Map();\n\n // Audio track subscription\n /** @internal */\n private audioCallbacks: AudioTrackCallbacks | null = null;\n /** @internal */\n private localAudioTrack: AgoraLocalAudioTrack | null = null;\n /** @internal */\n private remoteAudioTracks: Map<AgoraUID, AgoraRemoteAudioTrack> = new Map();\n\n // Debug mode\n /** @internal */\n private debugLogging = false;\n\n constructor(_options: AgoraProviderOptions = {}) {\n super();\n }\n\n /**\n * Enable or disable debug logging.\n * @param enabled - Whether to enable debug logging\n */\n setDebugLogging(enabled: boolean): void {\n this.debugLogging = enabled;\n if (this.seiExtractor) {\n this.seiExtractor.setDebugLogging(enabled);\n }\n }\n\n /**\n * Load Agora SDK dynamically.\n * @internal\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private async loadSDK(): Promise<any> {\n if (this.agoraSDK) {\n return this.agoraSDK;\n }\n\n try {\n // Direct dynamic import - Vite will handle this properly with optimizeDeps\n // Using 'any' type due to dynamic import type inference limitations\n const sdk = await import('agora-rtc-sdk-ng');\n // Agora SDK exports default as the main AgoraRTC object\n this.agoraSDK = sdk.default ?? sdk;\n return this.agoraSDK;\n } catch (error) {\n logger.error('Agora', 'Failed to load SDK:', error);\n throw new Error(\n '❌ Failed to load agora-rtc-sdk-ng.\\n' +\n 'Please ensure it is installed: pnpm add agora-rtc-sdk-ng'\n );\n }\n }\n\n async connect(config: RTCConnectionConfig): Promise<void> {\n if (!isAgoraConfig(config)) {\n throw new Error('AgoraProvider requires appId and channel in connection config');\n }\n\n const agoraConfig: AgoraConnectionConfig = config;\n\n const AgoraRTC = await this.loadSDK();\n\n // Enable SEI reception (required for animation data)\n AgoraRTC.setParameter('ENABLE_VIDEO_SEI', true);\n\n this.client = AgoraRTC.createClient({ \n mode: 'rtc', \n codec: 'h264' // Required for SEI support\n });\n\n this.setConnectionState('connecting');\n\n // Setup event listeners\n this.setupEventListeners(AgoraRTC);\n\n try {\n await this.client.join(\n agoraConfig.appId,\n agoraConfig.channel,\n agoraConfig.token || null,\n agoraConfig.uid ?? null\n );\n \n this.setConnectionState('connected');\n this.emit('connected');\n } catch (error) {\n this.setConnectionState('failed');\n this.emit('error', error as Error);\n throw error;\n }\n }\n\n /**\n * Setup Agora client event listeners\n * @internal\n */\n private setupEventListeners(_AgoraRTC: typeof import('agora-rtc-sdk-ng')): void {\n // Connection state changed\n this.client.on('connection-state-change', (curState: string, _revState: string) => {\n const mapConnectionState = (state: string): ConnectionState => {\n switch (state) {\n case 'DISCONNECTED':\n case 'DISCONNECTING':\n return ConnectionState.Disconnected;\n case 'CONNECTING':\n return ConnectionState.Connecting;\n case 'CONNECTED':\n return ConnectionState.Connected;\n case 'RECONNECTING':\n return ConnectionState.Reconnecting;\n default:\n return ConnectionState.Failed;\n }\n };\n \n this.setConnectionState(mapConnectionState(curState));\n \n if (curState === 'CONNECTED') {\n this.emit('connected');\n } else if (curState === 'DISCONNECTED') {\n this.cleanup();\n this.emit('disconnected');\n }\n });\n\n // User published (remote user joined and published tracks)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n this.client.on('user-published', async (user: any, mediaType: 'audio' | 'video') => {\n if (this.debugLogging) {\n logger.info('Agora', `User published: ${user.uid}, mediaType=${mediaType}`);\n }\n\n try {\n // Subscribe to the track\n await this.client.subscribe(user, mediaType);\n\n if (mediaType === 'video') {\n if (user.videoTrack) {\n this.handleVideoTrack(user, user.videoTrack);\n } else {\n logger.warn('Agora', `Video track is null after subscribe for user ${user.uid}`);\n }\n } else if (mediaType === 'audio') {\n if (user.audioTrack) {\n this.handleAudioTrack(user, user.audioTrack);\n } else {\n logger.warn('Agora', `Audio track is null after subscribe for user ${user.uid}`);\n }\n }\n } catch (error) {\n logger.error('Agora', `Failed to subscribe to ${mediaType} from ${user.uid}:`, error);\n this.emit('error', error as Error);\n }\n });\n\n // User unpublished (remote user stopped publishing)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n this.client.on('user-unpublished', (user: any, mediaType: 'audio' | 'video') => {\n if (this.debugLogging) {\n logger.info('Agora', `User unpublished: ${user.uid}, mediaType=${mediaType}`);\n }\n\n if (mediaType === 'video') {\n this.removeVideoContainer(user.uid);\n this.remoteUsers.delete(user.uid);\n } else if (mediaType === 'audio') {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const track = this.remoteAudioTracks.get(user.uid) as any;\n if (track) {\n track.stop();\n this.remoteAudioTracks.delete(user.uid);\n }\n \n if (this.audioCallbacks) {\n this.audioCallbacks.onAudioLost?.(user);\n }\n }\n });\n\n // User joined\n this.client.on('user-joined', (user: any) => {\n logger.info('Agora', `User joined: ${user.uid}`);\n });\n\n // User left\n this.client.on('user-left', (user: any, reason: string) => {\n logger.info('Agora', `User left: ${user.uid}, reason: ${reason}`);\n this.remoteUsers.delete(user.uid);\n const audioTrack = this.remoteAudioTracks.get(user.uid);\n if (audioTrack) {\n audioTrack.stop();\n this.remoteAudioTracks.delete(user.uid);\n }\n });\n\n // Error\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n this.client.on('exception', (error: any) => {\n logger.error('Agora', 'Exception:', error);\n this.emit('error', new Error(error.msg || String(error)));\n });\n }\n\n /**\n * Handle video track from remote user.\n * Sets up SEI event listeners and plays to hidden container.\n * @internal\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private handleVideoTrack(user: any, track: any): void {\n if (this.debugLogging) {\n logger.info('Agora', `Handling video track from ${user.uid}`);\n }\n\n // Listen for SEI data (animation frames)\n // Support both event names for SDK compatibility\n const seiHandler = (seiData: Uint8Array) => {\n if (this.seiExtractor) {\n this.seiExtractor.handleSEIData(seiData);\n }\n };\n\n // Try primary event name\n track.on('sei-received', seiHandler);\n // Also try alternative event name used in some SDK versions\n track.on('video-sei-received', seiHandler);\n\n // Play video to hidden element to ensure data flows\n // This is required for SEI events to be received\n const container = document.createElement('div');\n container.style.display = 'none';\n container.style.position = 'absolute';\n container.style.left = '-9999px';\n container.id = `agora-video-${user.uid}`;\n document.body.appendChild(container);\n this.videoContainers.set(user.uid, container);\n track.play(container);\n\n if (this.debugLogging) {\n logger.info('Agora', `Video track playing, SEI listeners attached for user ${user.uid}`);\n }\n\n // Store track reference\n const existingUser = this.remoteUsers.get(user.uid);\n if (existingUser) {\n existingUser.videoTrack = track;\n } else {\n this.remoteUsers.set(user.uid, { videoTrack: track });\n }\n }\n\n /**\n * Handle audio track from remote user.\n * @internal\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private handleAudioTrack(user: any, track: any): void {\n if (this.debugLogging) {\n logger.info('Agora', `Handling audio track from ${user.uid}`);\n }\n\n // Play audio\n track.play();\n this.remoteAudioTracks.set(user.uid, track);\n\n if (this.debugLogging) {\n logger.info('Agora', `Audio track playing for user ${user.uid}`);\n }\n\n if (this.audioCallbacks) {\n this.audioCallbacks.onAudioReceived?.(user);\n }\n }\n\n /**\n * Remove video container for a user.\n * @internal\n */\n private removeVideoContainer(uid: number): void {\n const container = this.videoContainers.get(uid);\n if (container) {\n container.remove();\n this.videoContainers.delete(uid);\n }\n }\n\n async disconnect(): Promise<void> {\n this.cleanup();\n if (this.client) {\n await this.client.leave();\n this.client = null;\n }\n this.setConnectionState('disconnected');\n this.emit('disconnected');\n }\n\n getConnectionState(): string {\n if (!this.client) {\n return 'disconnected';\n }\n return this.connectionState;\n }\n\n /** @internal */\n async subscribeAnimationTrack(callbacks: AnimationTrackCallbacks): Promise<void> {\n this._animationCallbacks = callbacks;\n \n // Create SEI extractor\n this.seiExtractor = new SEIExtractor();\n this.seiExtractor.setDebugLogging(this.debugLogging);\n this.seiExtractor.initialize(callbacks);\n \n // If already connected, check for existing remote video tracks\n if (this.client) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const remoteUsers = (this.client as any).remoteUsers;\n for (const user of remoteUsers) {\n if (user.videoTrack) {\n // Subscribe to SEI events with both event names for compatibility\n const seiHandler = (seiData: Uint8Array) => {\n if (this.seiExtractor) {\n this.seiExtractor.handleSEIData(seiData);\n }\n };\n user.videoTrack.on('sei-received', seiHandler);\n user.videoTrack.on('video-sei-received', seiHandler);\n }\n }\n }\n }\n\n /** @internal */\n async unsubscribeAnimationTrack(): Promise<void> {\n this._animationCallbacks = null;\n if (this.seiExtractor) {\n this.seiExtractor.dispose();\n this.seiExtractor = null;\n }\n \n // Remove SEI listeners from remote tracks\n for (const [uid, user] of this.remoteUsers) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const videoTrack = user.videoTrack as any;\n if (videoTrack) {\n videoTrack.off('sei-received');\n videoTrack.off('video-sei-received');\n }\n }\n \n // Remove video containers\n for (const [uid] of this.videoContainers) {\n this.removeVideoContainer(uid as number);\n }\n \n this.remoteUsers.clear();\n }\n\n /** @internal */\n async subscribeAudioTrack(callbacks: AudioTrackCallbacks): Promise<void> {\n this.audioCallbacks = callbacks;\n // Audio tracks are automatically handled in user-published event\n }\n\n /** @internal */\n async unsubscribeAudioTrack(): Promise<void> {\n this.audioCallbacks = null;\n this.remoteAudioTracks.forEach((track) => {\n track.stop();\n });\n this.remoteAudioTracks.clear();\n }\n\n async publishAudioTrack(track?: MediaStreamTrack): Promise<void> {\n if (!this.client) {\n throw new Error('Not connected to channel');\n }\n\n const AgoraRTC = await this.loadSDK();\n\n if (track) {\n // Create local audio track from provided MediaStreamTrack\n this.localAudioTrack = AgoraRTC.createCustomAudioTrack({\n mediaStreamTrack: track,\n });\n } else {\n // Create local audio track from microphone\n this.localAudioTrack = await AgoraRTC.createMicrophoneAudioTrack({\n encoderConfig: 'music_standard',\n AEC: true,\n ANS: true,\n AGC: true,\n });\n }\n\n // Publish the track\n await this.client.publish(this.localAudioTrack);\n }\n\n async unpublishAudioTrack(): Promise<void> {\n if (!this.client || !this.localAudioTrack) {\n return;\n }\n\n await this.client.unpublish(this.localAudioTrack);\n this.localAudioTrack.close();\n this.localAudioTrack = null;\n }\n\n /**\n * Play remote audio (resume playback)\n */\n playRemoteAudio(): void {\n this.remoteAudioTracks.forEach((track) => {\n track.play();\n });\n }\n\n /**\n * Pause remote audio\n */\n pauseRemoteAudio(): void {\n this.remoteAudioTracks.forEach((track) => {\n track.stop();\n });\n }\n\n /**\n * Get the native Agora RTC Client instance.\n * \n * Allows advanced users to access Agora-specific features\n * not exposed through the unified API.\n * \n * @returns The Agora IAgoraRTCClient instance, or null if not connected\n * \n * @example\n * ```typescript\n * const client = provider.getNativeClient();\n * if (client) {\n * // Access Agora-specific features\n * console.log('Connection state:', client.connectionState);\n * }\n * ```\n */\n getNativeClient(): AgoraClient | null {\n return this.client;\n }\n\n /**\n * Cleanup resources\n * @internal\n */\n private cleanup(): void {\n // Cleanup remote audio tracks\n this.remoteAudioTracks.forEach((track) => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (track as any).stop();\n });\n this.remoteAudioTracks.clear();\n \n // Cleanup remote video tracks and SEI listeners\n for (const [uid, user] of this.remoteUsers) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const videoTrack = user.videoTrack as any;\n if (videoTrack) {\n videoTrack.off('sei-received');\n videoTrack.off('video-sei-received');\n }\n }\n this.remoteUsers.clear();\n \n // Cleanup video containers\n for (const [uid] of this.videoContainers) {\n this.removeVideoContainer(uid as number);\n }\n \n // Cleanup SEI extractor\n if (this.seiExtractor) {\n this.seiExtractor.dispose();\n this.seiExtractor = null;\n }\n \n // Cleanup local audio track\n if (this.localAudioTrack) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this.localAudioTrack as any).close();\n this.localAudioTrack = null;\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index4.js","sources":["../src/providers/agora/AgoraProvider.ts"],"sourcesContent":["/**\n * Agora Provider Implementation.\n *\n * This provider uses Agora's H.264 SEI approach\n * to transport animation data.\n *\n * Key differences from LiveKit:\n * - Uses native SEI events instead of RTCRtpScriptTransform\n * - No ALR (Application-Level Redundancy) needed - Agora handles reliability\n * - Simpler data extraction - uses SEI header parsing\n *\n * @packageDocumentation\n */\n\nimport { BaseProvider } from '../base/BaseProvider';\nimport type { RTCConnectionConfig, AgoraConnectionConfig } from '../../types';\nimport { isAgoraConfig, ConnectionState } from '../../types';\nimport type { AnimationTrackCallbacks, AudioTrackCallbacks } from '../../core/types';\nimport { SEIExtractor } from './SEIExtractor';\nimport { logger } from '../../utils';\nimport type {\n AgoraClient,\n AgoraLocalAudioTrack,\n AgoraRemoteAudioTrack,\n AgoraRemoteVideoTrack,\n AgoraUID,\n} from './types';\n\n/**\n * Agora Provider options.\n * @internal Reserved for future use\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-interface\nexport interface AgoraProviderOptions {\n // Reserved for future configuration options\n}\n\n/**\n * Remote user info with tracks\n * @internal\n */\ninterface RemoteUserInfo {\n videoTrack?: AgoraRemoteVideoTrack;\n audioTrack?: AgoraRemoteAudioTrack;\n}\n\n/**\n * Agora Provider.\n *\n * Implements RTCProvider interface for Agora platform.\n * Uses native SEI events to receive animation data from H.264 video tracks.\n *\n * @example\n * ```typescript\n * import { AvatarPlayer } from '@spatialwalk/avatarkit-rtc';\n * import { AgoraProvider } from '@spatialwalk/avatarkit-rtc/providers/agora';\n *\n * const provider = new AgoraProvider();\n * const player = new AvatarPlayer(provider, renderer);\n *\n * await player.connect({\n * appId: 'your-agora-app-id',\n * channel: 'your-channel',\n * token: 'your-token',\n * });\n * ```\n */\nexport class AgoraProvider extends BaseProvider {\n /** Provider name identifier */\n readonly name = 'agora';\n\n /** @internal */\n private client: AgoraClient | null = null;\n /** @internal Dynamic SDK - type is any due to dynamic import */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private agoraSDK: any = null;\n\n // Animation track subscription (stored for cleanup, actual callbacks handled by SEIExtractor)\n /** @internal */\n private _animationCallbacks: AnimationTrackCallbacks | null = null;\n /** @internal */\n private seiExtractor: SEIExtractor | null = null;\n /** @internal */\n private remoteUsers: Map<AgoraUID, RemoteUserInfo> = new Map();\n /** @internal */\n private videoContainers: Map<AgoraUID, HTMLDivElement> = new Map();\n\n // Audio track subscription\n /** @internal */\n private audioCallbacks: AudioTrackCallbacks | null = null;\n /** @internal */\n private localAudioTrack: AgoraLocalAudioTrack | null = null;\n /** @internal */\n private remoteAudioTracks: Map<AgoraUID, AgoraRemoteAudioTrack> = new Map();\n\n // Debug mode\n /** @internal */\n private debugLogging = false;\n\n constructor(_options: AgoraProviderOptions = {}) {\n super();\n }\n\n /**\n * Enable or disable debug logging.\n * @param enabled - Whether to enable debug logging\n */\n setDebugLogging(enabled: boolean): void {\n this.debugLogging = enabled;\n if (this.seiExtractor) {\n this.seiExtractor.setDebugLogging(enabled);\n }\n }\n\n /**\n * Load Agora SDK dynamically.\n * @internal\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private async loadSDK(): Promise<any> {\n if (this.agoraSDK) {\n return this.agoraSDK;\n }\n\n try {\n // Direct dynamic import - Vite will handle this properly with optimizeDeps\n // Using 'any' type due to dynamic import type inference limitations\n const sdk = await import('agora-rtc-sdk-ng');\n // Agora SDK exports default as the main AgoraRTC object\n this.agoraSDK = sdk.default ?? sdk;\n return this.agoraSDK;\n } catch (error) {\n logger.error('Agora', 'Failed to load SDK:', error);\n throw new Error(\n '❌ Failed to load agora-rtc-sdk-ng.\\n' +\n 'Please ensure it is installed: pnpm add agora-rtc-sdk-ng'\n );\n }\n }\n\n async connect(config: RTCConnectionConfig): Promise<void> {\n if (!isAgoraConfig(config)) {\n throw new Error('AgoraProvider requires appId and channel in connection config');\n }\n\n const agoraConfig: AgoraConnectionConfig = config;\n\n const AgoraRTC = await this.loadSDK();\n\n // Enable SEI reception (required for animation data)\n AgoraRTC.setParameter('ENABLE_VIDEO_SEI', true);\n\n this.client = AgoraRTC.createClient({ \n mode: 'rtc', \n codec: 'h264' // Required for SEI support\n });\n\n this.setConnectionState('connecting');\n\n // Setup event listeners\n this.setupEventListeners(AgoraRTC);\n\n try {\n await this.client.join(\n agoraConfig.appId,\n agoraConfig.channel,\n agoraConfig.token || null,\n agoraConfig.uid ?? null\n );\n \n this.setConnectionState('connected');\n this.emit('connected');\n } catch (error) {\n this.setConnectionState('failed');\n this.emit('error', error as Error);\n throw error;\n }\n }\n\n /**\n * Setup Agora client event listeners\n * @internal\n */\n private setupEventListeners(_AgoraRTC: typeof import('agora-rtc-sdk-ng')): void {\n // Connection state changed\n this.client.on('connection-state-change', (curState: string, _revState: string) => {\n const mapConnectionState = (state: string): ConnectionState => {\n switch (state) {\n case 'DISCONNECTED':\n case 'DISCONNECTING':\n return ConnectionState.Disconnected;\n case 'CONNECTING':\n return ConnectionState.Connecting;\n case 'CONNECTED':\n return ConnectionState.Connected;\n case 'RECONNECTING':\n return ConnectionState.Reconnecting;\n default:\n return ConnectionState.Failed;\n }\n };\n \n this.setConnectionState(mapConnectionState(curState));\n \n if (curState === 'CONNECTED') {\n this.emit('connected');\n } else if (curState === 'DISCONNECTED') {\n this.cleanup();\n this.emit('disconnected');\n }\n });\n\n // User published (remote user joined and published tracks)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n this.client.on('user-published', async (user: any, mediaType: 'audio' | 'video') => {\n if (this.debugLogging) {\n logger.info('Agora', `User published: ${user.uid}, mediaType=${mediaType}`);\n }\n\n try {\n // Subscribe to the track\n await this.client.subscribe(user, mediaType);\n\n if (mediaType === 'video') {\n if (user.videoTrack) {\n this.handleVideoTrack(user, user.videoTrack);\n } else {\n logger.warn('Agora', `Video track is null after subscribe for user ${user.uid}`);\n }\n } else if (mediaType === 'audio') {\n if (user.audioTrack) {\n this.handleAudioTrack(user, user.audioTrack);\n } else {\n logger.warn('Agora', `Audio track is null after subscribe for user ${user.uid}`);\n }\n }\n } catch (error) {\n logger.error('Agora', `Failed to subscribe to ${mediaType} from ${user.uid}:`, error);\n this.emit('error', error as Error);\n }\n });\n\n // User unpublished (remote user stopped publishing)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n this.client.on('user-unpublished', (user: any, mediaType: 'audio' | 'video') => {\n if (this.debugLogging) {\n logger.info('Agora', `User unpublished: ${user.uid}, mediaType=${mediaType}`);\n }\n\n if (mediaType === 'video') {\n this.removeVideoContainer(user.uid);\n this.remoteUsers.delete(user.uid);\n } else if (mediaType === 'audio') {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const track = this.remoteAudioTracks.get(user.uid) as any;\n if (track) {\n track.stop();\n this.remoteAudioTracks.delete(user.uid);\n }\n \n if (this.audioCallbacks) {\n this.audioCallbacks.onAudioLost?.(user);\n }\n }\n });\n\n // User joined\n this.client.on('user-joined', (user: any) => {\n logger.info('Agora', `User joined: ${user.uid}`);\n });\n\n // User left\n this.client.on('user-left', (user: any, reason: string) => {\n logger.info('Agora', `User left: ${user.uid}, reason: ${reason}`);\n this.remoteUsers.delete(user.uid);\n const audioTrack = this.remoteAudioTracks.get(user.uid);\n if (audioTrack) {\n audioTrack.stop();\n this.remoteAudioTracks.delete(user.uid);\n }\n });\n\n // Error\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n this.client.on('exception', (error: any) => {\n logger.error('Agora', 'Exception:', error);\n this.emit('error', new Error(error.msg || String(error)));\n });\n }\n\n /**\n * Handle video track from remote user.\n * Sets up SEI event listeners and plays to hidden container.\n * @internal\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private handleVideoTrack(user: any, track: any): void {\n if (this.debugLogging) {\n logger.info('Agora', `Handling video track from ${user.uid}`);\n }\n\n // Listen for SEI data (animation frames)\n // Support both event names for SDK compatibility\n const seiHandler = (seiData: Uint8Array) => {\n if (this.seiExtractor) {\n this.seiExtractor.handleSEIData(seiData);\n }\n };\n\n // Try primary event name\n track.on('sei-received', seiHandler);\n // Also try alternative event name used in some SDK versions\n track.on('video-sei-received', seiHandler);\n\n // Play video to hidden element to ensure data flows\n // This is required for SEI events to be received\n const container = document.createElement('div');\n container.style.display = 'none';\n container.style.position = 'absolute';\n container.style.left = '-9999px';\n container.id = `agora-video-${user.uid}`;\n document.body.appendChild(container);\n this.videoContainers.set(user.uid, container);\n track.play(container);\n\n if (this.debugLogging) {\n logger.info('Agora', `Video track playing, SEI listeners attached for user ${user.uid}`);\n }\n\n // Store track reference\n const existingUser = this.remoteUsers.get(user.uid);\n if (existingUser) {\n existingUser.videoTrack = track;\n } else {\n this.remoteUsers.set(user.uid, { videoTrack: track });\n }\n }\n\n /**\n * Handle audio track from remote user.\n * @internal\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private handleAudioTrack(user: any, track: any): void {\n if (this.debugLogging) {\n logger.info('Agora', `Handling audio track from ${user.uid}`);\n }\n\n // Play audio\n track.play();\n this.remoteAudioTracks.set(user.uid, track);\n\n if (this.debugLogging) {\n logger.info('Agora', `Audio track playing for user ${user.uid}`);\n }\n\n if (this.audioCallbacks) {\n this.audioCallbacks.onAudioReceived?.(user);\n }\n }\n\n /**\n * Remove video container for a user.\n * @internal\n */\n private removeVideoContainer(uid: number): void {\n const container = this.videoContainers.get(uid);\n if (container) {\n container.remove();\n this.videoContainers.delete(uid);\n }\n }\n\n async disconnect(): Promise<void> {\n this.cleanup();\n if (this.client) {\n await this.client.leave();\n this.client = null;\n }\n this.setConnectionState('disconnected');\n this.emit('disconnected');\n }\n\n getConnectionState(): string {\n if (!this.client) {\n return 'disconnected';\n }\n return this.connectionState;\n }\n\n /** @internal */\n async subscribeAnimationTrack(callbacks: AnimationTrackCallbacks): Promise<void> {\n this._animationCallbacks = callbacks;\n \n // Create SEI extractor\n this.seiExtractor = new SEIExtractor();\n this.seiExtractor.setDebugLogging(this.debugLogging);\n this.seiExtractor.initialize(callbacks);\n \n // If already connected, check for existing remote video tracks\n if (this.client) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const remoteUsers = (this.client as any).remoteUsers;\n for (const user of remoteUsers) {\n if (user.videoTrack) {\n // Subscribe to SEI events with both event names for compatibility\n const seiHandler = (seiData: Uint8Array) => {\n if (this.seiExtractor) {\n this.seiExtractor.handleSEIData(seiData);\n }\n };\n user.videoTrack.on('sei-received', seiHandler);\n user.videoTrack.on('video-sei-received', seiHandler);\n }\n }\n }\n }\n\n /** @internal */\n async unsubscribeAnimationTrack(): Promise<void> {\n this._animationCallbacks = null;\n if (this.seiExtractor) {\n this.seiExtractor.dispose();\n this.seiExtractor = null;\n }\n \n // Remove SEI listeners from remote tracks\n for (const [uid, user] of this.remoteUsers) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const videoTrack = user.videoTrack as any;\n if (videoTrack) {\n videoTrack.off('sei-received');\n videoTrack.off('video-sei-received');\n }\n }\n \n // Remove video containers\n for (const [uid] of this.videoContainers) {\n this.removeVideoContainer(uid as number);\n }\n \n this.remoteUsers.clear();\n }\n\n /** @internal */\n async subscribeAudioTrack(callbacks: AudioTrackCallbacks): Promise<void> {\n this.audioCallbacks = callbacks;\n // Audio tracks are automatically handled in user-published event\n }\n\n /** @internal */\n async unsubscribeAudioTrack(): Promise<void> {\n this.audioCallbacks = null;\n this.remoteAudioTracks.forEach((track) => {\n track.stop();\n });\n this.remoteAudioTracks.clear();\n }\n\n async publishAudioTrack(track?: MediaStreamTrack): Promise<void> {\n if (!this.client) {\n throw new Error('Not connected to channel');\n }\n\n const AgoraRTC = await this.loadSDK();\n\n if (track) {\n // Create local audio track from provided MediaStreamTrack\n this.localAudioTrack = AgoraRTC.createCustomAudioTrack({\n mediaStreamTrack: track,\n });\n } else {\n // Create local audio track from microphone\n this.localAudioTrack = await AgoraRTC.createMicrophoneAudioTrack({\n encoderConfig: 'music_standard',\n AEC: true,\n ANS: true,\n AGC: true,\n });\n }\n\n // Publish the track\n await this.client.publish(this.localAudioTrack);\n }\n\n async unpublishAudioTrack(): Promise<void> {\n if (!this.client || !this.localAudioTrack) {\n return;\n }\n\n await this.client.unpublish(this.localAudioTrack);\n this.localAudioTrack.close();\n this.localAudioTrack = null;\n }\n\n /**\n * Get the native Agora RTC Client instance.\n * \n * Allows advanced users to access Agora-specific features\n * not exposed through the unified API.\n * \n * @returns The Agora IAgoraRTCClient instance, or null if not connected\n * \n * @example\n * ```typescript\n * const client = provider.getNativeClient();\n * if (client) {\n * // Access Agora-specific features\n * console.log('Connection state:', client.connectionState);\n * }\n * ```\n */\n getNativeClient(): AgoraClient | null {\n return this.client;\n }\n\n /**\n * Cleanup resources\n * @internal\n */\n private cleanup(): void {\n // Cleanup remote audio tracks\n this.remoteAudioTracks.forEach((track) => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (track as any).stop();\n });\n this.remoteAudioTracks.clear();\n \n // Cleanup remote video tracks and SEI listeners\n for (const [uid, user] of this.remoteUsers) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const videoTrack = user.videoTrack as any;\n if (videoTrack) {\n videoTrack.off('sei-received');\n videoTrack.off('video-sei-received');\n }\n }\n this.remoteUsers.clear();\n \n // Cleanup video containers\n for (const [uid] of this.videoContainers) {\n this.removeVideoContainer(uid as number);\n }\n \n // Cleanup SEI extractor\n if (this.seiExtractor) {\n this.seiExtractor.dispose();\n this.seiExtractor = null;\n }\n \n // Cleanup local audio track\n if (this.localAudioTrack) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this.localAudioTrack as any).close();\n this.localAudioTrack = null;\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
package/dist/index6.js
CHANGED
|
@@ -116,11 +116,11 @@ const _AnimationHandler = class _AnimationHandler {
|
|
|
116
116
|
if (!keyframes || keyframes.length === 0) {
|
|
117
117
|
return;
|
|
118
118
|
}
|
|
119
|
+
this.ensureSessionActive(frameSeq);
|
|
119
120
|
if (this.config.enableJitterBuffer && frameSeq !== void 0) {
|
|
120
121
|
this.bufferFrame(keyframes[0], frameSeq);
|
|
121
122
|
return;
|
|
122
123
|
}
|
|
123
|
-
this.renderedFrameCount++;
|
|
124
124
|
if (frameSeq !== void 0 && this.lastRenderedFrameSeq !== -1) {
|
|
125
125
|
if (frameSeq < this.lastRenderedFrameSeq) {
|
|
126
126
|
logger.warn(
|
|
@@ -141,7 +141,9 @@ const _AnimationHandler = class _AnimationHandler {
|
|
|
141
141
|
if (frameSeq !== void 0) {
|
|
142
142
|
this.lastRenderedFrameSeq = frameSeq;
|
|
143
143
|
}
|
|
144
|
+
this.renderedFrameCount++;
|
|
144
145
|
this.renderer.renderFrame(keyframes[0]);
|
|
146
|
+
this.logRenderedFrame("direct", frameSeq, isRecovered);
|
|
145
147
|
this.playbackFrameTimestamps.push(performance.now());
|
|
146
148
|
this.playbackFrameCount++;
|
|
147
149
|
if (frameSeq !== void 0) {
|
|
@@ -159,6 +161,10 @@ const _AnimationHandler = class _AnimationHandler {
|
|
|
159
161
|
* @internal
|
|
160
162
|
*/
|
|
161
163
|
async handleTransitionData(protobufData, frameCount) {
|
|
164
|
+
logger.info(
|
|
165
|
+
"AnimationHandler",
|
|
166
|
+
`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})`
|
|
167
|
+
);
|
|
162
168
|
if (this.hasHandledTransitionStart) {
|
|
163
169
|
return;
|
|
164
170
|
}
|
|
@@ -172,6 +178,14 @@ const _AnimationHandler = class _AnimationHandler {
|
|
|
172
178
|
logger.warn("AnimationHandler", "Renderer not ready for transition");
|
|
173
179
|
return;
|
|
174
180
|
}
|
|
181
|
+
if (this.isInSession && (this.lastRenderedFrameSeq >= 0 || this.frameBuffer.size > 0 || this.bufferState !== "direct")) {
|
|
182
|
+
this.hasHandledTransitionStart = true;
|
|
183
|
+
logger.warn(
|
|
184
|
+
"AnimationHandler",
|
|
185
|
+
`Ignoring late transition packet after playback start (lastRenderedSeq=${this.lastRenderedFrameSeq}, bufferState=${this.bufferState}, buffered=${this.frameBuffer.size})`
|
|
186
|
+
);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
175
189
|
const keyframes = this.decoder(protobufData);
|
|
176
190
|
if (!keyframes || keyframes.length === 0) {
|
|
177
191
|
logger.warn("AnimationHandler", "No target keyframe in transition data");
|
|
@@ -179,11 +193,7 @@ const _AnimationHandler = class _AnimationHandler {
|
|
|
179
193
|
}
|
|
180
194
|
this.hasHandledTransitionStart = true;
|
|
181
195
|
this.hasHandledTransitionEnd = false;
|
|
182
|
-
this.
|
|
183
|
-
this.lastFrameReceivedTime = Date.now();
|
|
184
|
-
this.hasReportedStall = false;
|
|
185
|
-
this.startWatchdog();
|
|
186
|
-
this.startPlaybackStats();
|
|
196
|
+
this.ensureSessionActive();
|
|
187
197
|
const targetFrame = keyframes[0];
|
|
188
198
|
const frames = frameCount ?? this.config.transitionStartFrameCount;
|
|
189
199
|
logger.info("AnimationHandler", `Generating ${frames} transition frames to target`);
|
|
@@ -202,6 +212,7 @@ const _AnimationHandler = class _AnimationHandler {
|
|
|
202
212
|
} catch (error) {
|
|
203
213
|
logger.error("AnimationHandler", "Failed to generate transition:", error);
|
|
204
214
|
this.renderer.renderFrame(targetFrame);
|
|
215
|
+
this.logRenderedFrame("transition-fallback");
|
|
205
216
|
} finally {
|
|
206
217
|
this.isGeneratingStartTransition = false;
|
|
207
218
|
}
|
|
@@ -226,12 +237,14 @@ const _AnimationHandler = class _AnimationHandler {
|
|
|
226
237
|
if (!this.renderer.isReady()) {
|
|
227
238
|
logger.warn("AnimationHandler", "Renderer not ready for transition to idle");
|
|
228
239
|
this.renderer.renderFrame(void 0, true);
|
|
240
|
+
this.logRenderedFrame("idle");
|
|
229
241
|
return;
|
|
230
242
|
}
|
|
231
243
|
const keyframes = this.decoder(protobufData);
|
|
232
244
|
if (!keyframes || keyframes.length === 0) {
|
|
233
245
|
logger.warn("AnimationHandler", "No last keyframe in transition end data, starting idle directly");
|
|
234
246
|
this.renderer.renderFrame(void 0, true);
|
|
247
|
+
this.logRenderedFrame("idle");
|
|
235
248
|
return;
|
|
236
249
|
}
|
|
237
250
|
this.hasHandledTransitionEnd = true;
|
|
@@ -254,6 +267,7 @@ const _AnimationHandler = class _AnimationHandler {
|
|
|
254
267
|
} catch (error) {
|
|
255
268
|
logger.error("AnimationHandler", "Failed to generate reverse transition:", error);
|
|
256
269
|
this.renderer.renderFrame(void 0, true);
|
|
270
|
+
this.logRenderedFrame("idle");
|
|
257
271
|
} finally {
|
|
258
272
|
this.isGeneratingEndTransition = false;
|
|
259
273
|
}
|
|
@@ -266,6 +280,24 @@ const _AnimationHandler = class _AnimationHandler {
|
|
|
266
280
|
this.isInSession = false;
|
|
267
281
|
this.hasReportedStall = false;
|
|
268
282
|
this.renderer.renderFrame(void 0, true);
|
|
283
|
+
this.logRenderedFrame("idle");
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Ensure session-level timers/stats are active.
|
|
287
|
+
* @internal
|
|
288
|
+
*/
|
|
289
|
+
ensureSessionActive(frameSeq) {
|
|
290
|
+
if (this.isInSession) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
this.isInSession = true;
|
|
294
|
+
this.lastFrameReceivedTime = Date.now();
|
|
295
|
+
this.hasReportedStall = false;
|
|
296
|
+
this.startWatchdog();
|
|
297
|
+
this.startPlaybackStats();
|
|
298
|
+
if (frameSeq !== void 0) {
|
|
299
|
+
logger.info("AnimationHandler", `Session started from animation frame seq=${frameSeq}`);
|
|
300
|
+
}
|
|
269
301
|
}
|
|
270
302
|
/**
|
|
271
303
|
* Reset animation frame tracking (call on session start).
|
|
@@ -438,6 +470,20 @@ const _AnimationHandler = class _AnimationHandler {
|
|
|
438
470
|
* @internal
|
|
439
471
|
*/
|
|
440
472
|
bufferFrame(flame, seq) {
|
|
473
|
+
if (this.lastRenderedFrameSeq >= 0 && seq <= this.lastRenderedFrameSeq) {
|
|
474
|
+
logger.warn(
|
|
475
|
+
"AnimationHandler",
|
|
476
|
+
`Jitter buffer: dropping stale frame seq=${seq} (lastRendered=${this.lastRenderedFrameSeq})`
|
|
477
|
+
);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (this.bufferNextSeq >= 0 && seq < this.bufferNextSeq) {
|
|
481
|
+
logger.warn(
|
|
482
|
+
"AnimationHandler",
|
|
483
|
+
`Jitter buffer: dropping late frame seq=${seq} (nextExpected=${this.bufferNextSeq})`
|
|
484
|
+
);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
441
487
|
if (this.frameBuffer.has(seq)) {
|
|
442
488
|
return;
|
|
443
489
|
}
|
|
@@ -448,6 +494,10 @@ const _AnimationHandler = class _AnimationHandler {
|
|
|
448
494
|
if (k < oldestSeq) oldestSeq = k;
|
|
449
495
|
}
|
|
450
496
|
this.frameBuffer.delete(oldestSeq);
|
|
497
|
+
logger.warn(
|
|
498
|
+
"AnimationHandler",
|
|
499
|
+
`Jitter buffer: overflow, dropping seq=${oldestSeq} (nextExpected=${this.bufferNextSeq})`
|
|
500
|
+
);
|
|
451
501
|
}
|
|
452
502
|
switch (this.bufferState) {
|
|
453
503
|
case "direct":
|
|
@@ -470,6 +520,45 @@ const _AnimationHandler = class _AnimationHandler {
|
|
|
470
520
|
break;
|
|
471
521
|
}
|
|
472
522
|
}
|
|
523
|
+
/**
|
|
524
|
+
* Drop buffered frames that are now too old to ever be rendered in-order.
|
|
525
|
+
* @internal
|
|
526
|
+
*/
|
|
527
|
+
dropStaleBufferedFrames() {
|
|
528
|
+
if (this.frameBuffer.size === 0) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
const minAllowedSeq = Math.max(this.bufferNextSeq, this.lastRenderedFrameSeq + 1);
|
|
532
|
+
if (minAllowedSeq < 0) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
let dropped = 0;
|
|
536
|
+
for (const seq of Array.from(this.frameBuffer.keys())) {
|
|
537
|
+
if (seq < minAllowedSeq) {
|
|
538
|
+
this.frameBuffer.delete(seq);
|
|
539
|
+
dropped++;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (dropped > 0) {
|
|
543
|
+
logger.warn(
|
|
544
|
+
"AnimationHandler",
|
|
545
|
+
`Jitter buffer: dropped ${dropped} stale frame(s) older than seq=${minAllowedSeq}`
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Find the lowest buffered sequence at or after minSeq.
|
|
551
|
+
* @internal
|
|
552
|
+
*/
|
|
553
|
+
findLowestBufferedSeqAtOrAfter(minSeq) {
|
|
554
|
+
let candidate = Infinity;
|
|
555
|
+
for (const seq of this.frameBuffer.keys()) {
|
|
556
|
+
if (seq >= minSeq && seq < candidate) {
|
|
557
|
+
candidate = seq;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return candidate === Infinity ? null : candidate;
|
|
561
|
+
}
|
|
473
562
|
/**
|
|
474
563
|
* Begin draining the buffer at 25fps.
|
|
475
564
|
* @internal
|
|
@@ -497,28 +586,31 @@ const _AnimationHandler = class _AnimationHandler {
|
|
|
497
586
|
if (this.bufferState !== "draining") {
|
|
498
587
|
return;
|
|
499
588
|
}
|
|
589
|
+
this.dropStaleBufferedFrames();
|
|
500
590
|
const frame = this.frameBuffer.get(this.bufferNextSeq);
|
|
501
591
|
if (frame) {
|
|
502
592
|
this.renderBufferedFrame(frame);
|
|
503
593
|
this.frameBuffer.delete(this.bufferNextSeq);
|
|
504
594
|
this.bufferNextSeq++;
|
|
505
595
|
} else if (this.frameBuffer.size > 0) {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
596
|
+
const nextSeq = this.findLowestBufferedSeqAtOrAfter(this.bufferNextSeq);
|
|
597
|
+
if (nextSeq === null) {
|
|
598
|
+
this.bufferState = "starved";
|
|
599
|
+
logger.warn("AnimationHandler", "Jitter buffer: no in-order frames available, pausing drain");
|
|
600
|
+
return;
|
|
509
601
|
}
|
|
510
|
-
const
|
|
511
|
-
const waitTime = performance.now() -
|
|
602
|
+
const nextFrame = this.frameBuffer.get(nextSeq);
|
|
603
|
+
const waitTime = performance.now() - nextFrame.receivedAt;
|
|
512
604
|
if (waitTime > this.config.maxBufferDelayMs) {
|
|
513
|
-
const gap =
|
|
605
|
+
const gap = Math.max(0, nextSeq - this.bufferNextSeq);
|
|
514
606
|
this.playbackGapCount += gap;
|
|
515
607
|
logger.warn(
|
|
516
608
|
"AnimationHandler",
|
|
517
|
-
`Jitter buffer: skipping ${gap} frame(s) from seq ${this.bufferNextSeq} to ${
|
|
609
|
+
`Jitter buffer: skipping ${gap} frame(s) from seq ${this.bufferNextSeq} to ${nextSeq} after ${waitTime.toFixed(1)}ms`
|
|
518
610
|
);
|
|
519
|
-
this.renderBufferedFrame(
|
|
520
|
-
this.frameBuffer.delete(
|
|
521
|
-
this.bufferNextSeq =
|
|
611
|
+
this.renderBufferedFrame(nextFrame);
|
|
612
|
+
this.frameBuffer.delete(nextSeq);
|
|
613
|
+
this.bufferNextSeq = nextSeq + 1;
|
|
522
614
|
}
|
|
523
615
|
} else {
|
|
524
616
|
this.bufferState = "starved";
|
|
@@ -536,9 +628,17 @@ const _AnimationHandler = class _AnimationHandler {
|
|
|
536
628
|
* @internal
|
|
537
629
|
*/
|
|
538
630
|
renderBufferedFrame(frame) {
|
|
631
|
+
if (this.lastRenderedFrameSeq >= 0 && frame.seq <= this.lastRenderedFrameSeq) {
|
|
632
|
+
logger.warn(
|
|
633
|
+
"AnimationHandler",
|
|
634
|
+
`Jitter buffer: refusing out-of-order render seq=${frame.seq} (lastRendered=${this.lastRenderedFrameSeq})`
|
|
635
|
+
);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
539
638
|
this.renderer.renderFrame(frame.flame);
|
|
540
639
|
this.lastRenderedFrameSeq = frame.seq;
|
|
541
640
|
this.renderedFrameCount++;
|
|
641
|
+
this.logRenderedFrame("buffer", frame.seq);
|
|
542
642
|
this.playbackFrameTimestamps.push(performance.now());
|
|
543
643
|
this.playbackFrameCount++;
|
|
544
644
|
}
|
|
@@ -575,6 +675,7 @@ const _AnimationHandler = class _AnimationHandler {
|
|
|
575
675
|
if (wasTransitioningToIdle) {
|
|
576
676
|
logger.info("AnimationHandler", "Starting idle animation after transition");
|
|
577
677
|
this.renderer.renderFrame(void 0, true);
|
|
678
|
+
this.logRenderedFrame("idle");
|
|
578
679
|
}
|
|
579
680
|
return;
|
|
580
681
|
}
|
|
@@ -585,6 +686,7 @@ const _AnimationHandler = class _AnimationHandler {
|
|
|
585
686
|
}
|
|
586
687
|
const frame = this.transitionFrames[this.transitionFrameIndex];
|
|
587
688
|
this.renderer.renderFrame(frame);
|
|
689
|
+
this.logRenderedFrame("transition");
|
|
588
690
|
this.transitionFrameIndex++;
|
|
589
691
|
this.transitionTimeoutId = setTimeout(() => {
|
|
590
692
|
if (this.isPlayingTransition) {
|
|
@@ -592,6 +694,16 @@ const _AnimationHandler = class _AnimationHandler {
|
|
|
592
694
|
}
|
|
593
695
|
}, 40);
|
|
594
696
|
}
|
|
697
|
+
/**
|
|
698
|
+
* Emit a per-frame render log for debugging ordering issues.
|
|
699
|
+
* @internal
|
|
700
|
+
*/
|
|
701
|
+
logRenderedFrame(source, seq, isRecovered) {
|
|
702
|
+
logger.info(
|
|
703
|
+
"AnimationHandler",
|
|
704
|
+
`Rendered frame: source=${source}, seq=${seq ?? "n/a"}${isRecovered ? " [RECOVERED]" : ""}`
|
|
705
|
+
);
|
|
706
|
+
}
|
|
595
707
|
};
|
|
596
708
|
/** @internal */
|
|
597
709
|
__publicField(_AnimationHandler, "STALL_TIMEOUT_MS", 3e3);
|