@spatialwalk/avatarkit-rtc 1.0.0-beta.1 → 1.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -410
- package/dist/assets/animation-worker-DOGeTjF0.js.map +1 -0
- package/dist/core/AvatarPlayer.d.ts +96 -12
- package/dist/core/AvatarPlayer.d.ts.map +1 -1
- package/dist/core/RTCProvider.d.ts +12 -16
- package/dist/core/RTCProvider.d.ts.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index10.js +91 -39
- package/dist/index10.js.map +1 -1
- package/dist/index11.js +14 -386
- package/dist/index11.js.map +1 -1
- package/dist/index12.js +350 -64
- package/dist/index12.js.map +1 -1
- package/dist/index13.js +44 -14
- package/dist/index13.js.map +1 -1
- package/dist/index14.js +25 -44
- package/dist/index14.js.map +1 -1
- package/dist/index2.js +335 -46
- package/dist/index2.js.map +1 -1
- package/dist/index3.js +265 -54
- package/dist/index3.js.map +1 -1
- package/dist/index4.js +105 -86
- package/dist/index4.js.map +1 -1
- package/dist/index5.js +6 -2
- package/dist/index5.js.map +1 -1
- package/dist/index6.js +603 -39
- package/dist/index6.js.map +1 -1
- package/dist/index8.js +128 -167
- package/dist/index8.js.map +1 -1
- package/dist/index9.js +65 -164
- package/dist/index9.js.map +1 -1
- package/dist/providers/agora/AgoraProvider.d.ts +0 -13
- package/dist/providers/agora/AgoraProvider.d.ts.map +1 -1
- package/dist/providers/agora/index.d.ts +1 -5
- package/dist/providers/agora/index.d.ts.map +1 -1
- package/dist/providers/agora/types.d.ts.map +1 -1
- package/dist/providers/base/BaseProvider.d.ts +50 -8
- package/dist/providers/base/BaseProvider.d.ts.map +1 -1
- package/dist/providers/livekit/LiveKitProvider.d.ts +4 -15
- package/dist/providers/livekit/LiveKitProvider.d.ts.map +1 -1
- package/dist/providers/livekit/animation-worker.d.ts.map +1 -1
- package/dist/providers/livekit/index.d.ts +1 -5
- package/dist/providers/livekit/index.d.ts.map +1 -1
- package/dist/types/index.d.ts +21 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/telemetry.d.ts +22 -0
- package/dist/utils/telemetry.d.ts.map +1 -0
- package/package.json +21 -12
- package/dist/assets/animation-worker-CUXZycUw.js.map +0 -1
- package/dist/index15.js +0 -29
- package/dist/index15.js.map +0 -1
- package/dist/index16.js +0 -144
- package/dist/index16.js.map +0 -1
- package/dist/index17.js +0 -106
- package/dist/index17.js.map +0 -1
- package/dist/index18.js +0 -28
- package/dist/index18.js.map +0 -1
- package/dist/proto/animation.d.ts +0 -12
- package/dist/proto/animation.d.ts.map +0 -1
package/dist/index2.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index2.js","sources":["../src/core/AvatarPlayer.ts"],"sourcesContent":["/**\n * AvatarPlayer - Unified entry point for RTC avatar streaming.\n *\n * This class provides a unified API that shields applications from\n * differences between different RTC providers (LiveKit, Agora, etc.)\n * and integrates seamlessly with @spatialwalk/avatarkit for rendering.\n *\n * @packageDocumentation\n */\n\nimport type { RTCProvider } from './RTCProvider';\nimport type { RTCConnectionConfig } from '../types';\nimport { AnimationHandler } from './AnimationHandler';\nimport type { AnimationTrackCallbacks, RTCProviderEvents } from './types';\nimport { configureLogger, type LogLevel } from '../utils';\n\n// Import AvatarView type from avatarkit\nimport type { AvatarView, KeyframeData } from '@spatialwalk/avatarkit';\n\n/**\n * Options for configuring AvatarPlayer.\n */\nexport interface AvatarPlayerOptions {\n /**\n * Log level for SDK output.\n * - 'info': Show all logs (debug mode)\n * - 'warning': Show warnings and errors (default)\n * - 'error': Show only errors\n * - 'none': Disable all logs\n */\n logLevel?: LogLevel;\n}\n\n/**\n * Unified Avatar Player.\n *\n * Main entry point for applications to interact with RTC avatar streaming.\n * Handles connection management, audio publishing, animation rendering,\n * and coordinates with avatarkit for display.\n *\n * @example\n * ```typescript\n * import { AvatarPlayer, LiveKitProvider } from '@spatialwalk/avatarkit-rtc';\n * import { AvatarView, Avatar } from '@spatialwalk/avatarkit';\n *\n * // Create avatar and view (from avatarkit)\n * const avatar = await Avatar.create(characterId);\n * const avatarView = new AvatarView(avatar, container);\n *\n * // Create player\n * const provider = new LiveKitProvider();\n * const player = new AvatarPlayer(provider, avatarView);\n *\n * // Connect and start\n * await player.connect({ url: 'wss://...', token: '...' });\n * await player.startPublishing(); // Start microphone\n *\n * // Disconnect when done\n * await player.disconnect();\n * ```\n */\nexport class AvatarPlayer {\n /** @internal */\n private provider: RTCProvider;\n\n /** @internal */\n private avatarView: AvatarView;\n\n /** @internal */\n private animationHandler: AnimationHandler;\n\n /** @internal */\n private _isConnected = false;\n\n /** @internal */\n private localAudioTrack: MediaStreamTrack | null = null;\n\n /** @internal */\n private mediaStream: MediaStream | null = null;\n\n /**\n * Create a new AvatarPlayer instance.\n * @param provider - RTC provider implementation (e.g., LiveKitProvider)\n * @param avatarView - AvatarView instance from @spatialwalk/avatarkit\n * @param options - Optional configuration for transitions\n */\n constructor(\n provider: RTCProvider,\n avatarView: AvatarView,\n options?: AvatarPlayerOptions\n ) {\n this.provider = provider;\n this.avatarView = avatarView;\n\n // Configure logging if specified\n if (options?.logLevel !== undefined) {\n configureLogger({ level: options.logLevel });\n }\n\n // Create internal renderer adapter\n const renderer = this.createRendererAdapter();\n\n this.animationHandler = new AnimationHandler(renderer);\n\n // Setup provider event handlers\n this.setupProviderEvents();\n }\n\n /**\n * Check if currently connected to RTC server.\n */\n get isConnected(): boolean {\n return this._isConnected;\n }\n\n /**\n * Connect to RTC server.\n * @param config - Connection configuration (URL, token, room name, etc.)\n * @throws Error if already connected or connection fails\n */\n async connect(config: RTCConnectionConfig): Promise<void> {\n if (this._isConnected) {\n throw new Error('Already connected. Please disconnect first.');\n }\n\n // Setup animation callbacks before connecting\n await this.setupAnimationCallbacks();\n await this.provider.connect(config);\n this._isConnected = true;\n }\n\n /**\n * Disconnect from RTC server.\n * Stops all tracks and cleans up resources.\n */\n async disconnect(): Promise<void> {\n if (!this._isConnected) {\n return;\n }\n\n // Stop publishing if active\n if (this.localAudioTrack) {\n await this.stopPublishing();\n }\n\n await this.provider.disconnect();\n this.animationHandler.dispose();\n this._isConnected = false;\n }\n\n /**\n * Start publishing microphone audio to the RTC server.\n * Requests microphone permission and starts publishing.\n * @throws Error if permission denied or no microphone found\n */\n async startPublishing(): Promise<void> {\n if (this.localAudioTrack) {\n return; // Already publishing\n }\n\n // Request microphone permission\n this.mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });\n this.localAudioTrack = this.mediaStream.getAudioTracks()[0];\n\n await this.provider.publishAudioTrack(this.localAudioTrack);\n }\n\n /**\n * Stop publishing microphone audio.\n */\n async stopPublishing(): Promise<void> {\n if (!this.localAudioTrack) {\n return;\n }\n\n await this.provider.unpublishAudioTrack();\n\n // Stop and cleanup tracks\n this.localAudioTrack.stop();\n this.localAudioTrack = null;\n\n if (this.mediaStream) {\n this.mediaStream.getTracks().forEach((track) => track.stop());\n this.mediaStream = null;\n }\n }\n\n /**\n * Get current connection state.\n * @returns Connection state string (e.g., 'connected', 'disconnected')\n */\n getConnectionState(): string {\n return this.provider.getConnectionState();\n }\n\n /**\n * Get the native RTC client object.\n * \n * Returns the underlying RTC client for advanced use cases:\n * - LiveKit: Returns the Room instance\n * - Agora: Returns the IAgoraRTCClient instance\n * \n * @returns The native RTC client object, or null if not connected\n * \n * @example\n * ```typescript\n * // Access LiveKit Room\n * const room = player.getNativeClient() as Room;\n * console.log('Participants:', room?.remoteParticipants.size);\n * \n * // Access Agora Client\n * const client = player.getNativeClient() as IAgoraRTCClient;\n * console.log('State:', client?.connectionState);\n * ```\n */\n getNativeClient(): unknown {\n return this.provider.getNativeClient();\n }\n\n /**\n * Add event listener.\n * @param event - Event name ('connected', 'disconnected', 'error', etc.)\n * @param handler - Event handler function\n */\n on<K extends keyof RTCProviderEvents>(\n event: K,\n handler: RTCProviderEvents[K]\n ): void {\n this.provider.on(event, handler);\n }\n\n /**\n * Remove event listener.\n * @param event - Event name\n * @param handler - Event handler function (must be same reference as added)\n */\n off<K extends keyof RTCProviderEvents>(\n event: K,\n handler: RTCProviderEvents[K]\n ): void {\n this.provider.off(event, handler);\n }\n\n /**\n * Create internal renderer adapter that bridges AnimationHandler to AvatarView.\n * @internal\n */\n private createRendererAdapter() {\n const avatarView = this.avatarView;\n\n return {\n renderFrame(flame: KeyframeData | undefined, startIdle?: boolean): void {\n if (startIdle) {\n // Enable idle rendering mode\n avatarView.renderFlame(undefined as unknown as KeyframeData, true);\n } else if (flame) {\n // Render the animation frame\n avatarView.renderFlame(flame);\n }\n },\n\n async generateTransitionFromIdle(\n targetFrame: KeyframeData,\n frameCount: number\n ): Promise<KeyframeData[]> {\n // Use avatarkit's generateTransition with from=undefined (current idle frame)\n return avatarView.generateTransition({\n to: targetFrame,\n frameCount,\n useLinear: true, // Linear interpolation for idle->speaking\n });\n },\n\n isReady(): boolean {\n return true; // AvatarView is always ready once created\n },\n };\n }\n\n /**\n * Setup animation track callbacks.\n * @internal\n */\n private async setupAnimationCallbacks(): Promise<void> {\n const callbacks: AnimationTrackCallbacks = {\n onAnimationData: (protobufData, metadata) => {\n // Reset tracking on session start\n if (metadata.isStart) {\n this.animationHandler.resetTracking();\n // Start audio playback on session start (fallback if transition didn't trigger it)\n this.provider.playRemoteAudio();\n }\n\n this.animationHandler.handleAnimationData(\n protobufData,\n metadata.frameSeq,\n metadata.isRecovered\n );\n },\n onTransition: (protobufData, transitionFrameCount) => {\n this.provider.pauseRemoteAudio();\n this.animationHandler.handleTransitionData(protobufData, transitionFrameCount);\n },\n onTransitionEnd: (protobufData, transitionFrameCount) => {\n this.animationHandler.handleTransitionToIdle(protobufData, transitionFrameCount);\n },\n onSessionStart: () => {\n // Session started - can be used for UI feedback\n },\n onSessionEnd: () => {\n // Session ended - can be used for UI feedback\n },\n onIdleStart: () => {\n this.animationHandler.startIdle();\n },\n onStreamStats: () => {\n // Stats can be forwarded to application layer if needed\n },\n };\n\n await this.provider.subscribeAnimationTrack(callbacks);\n }\n\n /**\n * Setup provider event handlers.\n * @internal\n */\n private setupProviderEvents(): void {\n this.provider.on('disconnected', () => {\n this._isConnected = false;\n // Return to idle animation when disconnected\n this.animationHandler.startIdle();\n });\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index2.js","sources":["../src/core/AvatarPlayer.ts"],"sourcesContent":["/**\n * AvatarPlayer - Unified entry point for RTC avatar streaming.\n *\n * This class provides a unified API that shields applications from\n * differences between different RTC providers (LiveKit, Agora, etc.)\n * and integrates seamlessly with @spatialwalk/avatarkit for rendering.\n *\n * @packageDocumentation\n */\n\nimport type { RTCProvider } from './RTCProvider';\nimport type { RTCConnectionConfig, RTCPrepareConnectionConfig } from '../types';\nimport { AnimationHandler } from './AnimationHandler';\nimport type { AnimationTrackCallbacks } from './types';\nimport { configureLogger, logger, type LogLevel, initializeTelemetry, trackEvent, flushTelemetry } from '../utils';\n\n// Import AvatarView type from avatarkit\nimport type { AvatarView } from '@spatialwalk/avatarkit';\n\n/**\n * Options for configuring AvatarPlayer.\n */\nexport interface AvatarPlayerOptions {\n /**\n * Log level for SDK output.\n * - 'info': Show all logs (debug mode)\n * - 'warning': Show warnings and errors (default)\n * - 'error': Show only errors\n * - 'none': Disable all logs\n */\n logLevel?: LogLevel;\n\n /**\n * Enable jitter buffer for smoother animation playback.\n * When enabled, frames are buffered and rendered in sequence order at a steady 25fps,\n * absorbing network jitter and out-of-order delivery.\n * Default: true\n */\n enableJitterBuffer?: boolean;\n\n /**\n * Maximum delay (in ms) a frame can sit in the jitter buffer before being rendered.\n * Only used when enableJitterBuffer is true.\n * Default: 80 (2 frames at 25fps)\n */\n maxBufferDelayMs?: number;\n}\n\n/**\n * Unified Avatar Player.\n *\n * Main entry point for applications to interact with RTC avatar streaming.\n * Handles connection management, audio publishing, animation rendering,\n * and coordinates with avatarkit for display.\n *\n * @example\n * ```typescript\n * import { AvatarPlayer, LiveKitProvider } from '@spatialwalk/avatarkit-rtc';\n * import { AvatarView, Avatar } from '@spatialwalk/avatarkit';\n *\n * // Create avatar and view (from avatarkit)\n * const avatar = await Avatar.create(characterId);\n * const avatarView = new AvatarView(avatar, container);\n *\n * // Create player\n * const provider = new LiveKitProvider();\n * const player = new AvatarPlayer(provider, avatarView);\n *\n * // Connect\n * await player.connect({ url: 'wss://...', token: '...' });\n *\n * // Option 1: Use microphone (most common)\n * await player.startPublishing();\n * await player.stopPublishing();\n *\n * // Option 2: Use custom audio source\n * const stream = await navigator.mediaDevices.getUserMedia({ audio: true });\n * await player.publishAudio(stream.getAudioTracks()[0]);\n * await player.unpublishAudio();\n * stream.getTracks().forEach(t => t.stop());\n *\n * // Disconnect\n * await player.disconnect();\n * ```\n */\nexport class AvatarPlayer {\n /** @internal */\n private provider: RTCProvider;\n\n /** @internal */\n private avatarView: AvatarView;\n\n /** @internal */\n private animationHandler: AnimationHandler;\n\n /** @internal */\n private _isConnected = false;\n\n /** @internal */\n private localAudioTrack: MediaStreamTrack | null = null;\n\n /** @internal */\n private microphoneStream: MediaStream | null = null;\n\n /** @internal */\n private eventHandlers: Map<string, Set<(...args: unknown[]) => void>> =\n new Map();\n\n /** @internal */\n private lastConnectionConfig: RTCConnectionConfig | null = null;\n\n /** @internal */\n private isReconnecting = false;\n\n /** @internal */\n private hasActiveAnimationSession = false;\n\n /** @internal */\n private isTrackingPrimed = false;\n\n /** @internal */\n private connectStartTime = 0;\n\n /** @internal */\n private sessionStartTime = 0;\n\n /** @internal */\n private stallCount = 0;\n\n /** @internal */\n private reconnectCount = 0;\n\n /** @internal */\n private conversationCount = 0;\n\n /** @internal */\n private providerName = '';\n\n /**\n * Create a new AvatarPlayer instance.\n * @param provider - RTC provider implementation (e.g., LiveKitProvider)\n * @param avatarView - AvatarView instance from @spatialwalk/avatarkit\n * @param options - Optional configuration for transitions\n */\n constructor(\n provider: RTCProvider,\n avatarView: AvatarView,\n options?: AvatarPlayerOptions,\n ) {\n this.provider = provider;\n this.avatarView = avatarView;\n this.providerName = provider.name;\n\n // Configure logging if specified\n if (options?.logLevel !== undefined) {\n configureLogger({ level: options.logLevel });\n }\n\n // Initialize telemetry (piggybacks on avatarkit's PostHog instance)\n initializeTelemetry({ sdkVersion: __RTC_SDK_VERSION__ });\n logger.info('AvatarPlayer', `SDK version: ${__RTC_SDK_VERSION__}`);\n\n // Create internal renderer adapter\n const renderer = this.createRendererAdapter();\n\n this.animationHandler = new AnimationHandler(renderer, {\n enableJitterBuffer: options?.enableJitterBuffer,\n maxBufferDelayMs: options?.maxBufferDelayMs,\n providerName: this.providerName,\n onStreamStalled: () => {\n // AnimationHandler already switches to idle state\n // Just notify external listeners, let them decide what to do (e.g., call reconnect())\n this.hasActiveAnimationSession = false;\n this.isTrackingPrimed = false;\n this.stallCount++;\n trackEvent('rtc_stream_stalled', 'warning', {\n provider: this.providerName,\n session_elapsed: this.sessionStartTime ? Date.now() - this.sessionStartTime : 0,\n });\n this.emit('stalled');\n },\n });\n\n // Setup provider event handlers\n this.setupProviderEvents();\n }\n\n /**\n * Check if currently connected to RTC server.\n */\n get isConnected(): boolean {\n return this._isConnected;\n }\n\n /**\n * Connect to RTC server.\n * @param config - Connection configuration (URL, token, room name, etc.)\n * @throws Error if already connected or connection fails\n */\n async connect(config: RTCConnectionConfig): Promise<void> {\n if (this._isConnected) {\n throw new Error('Already connected. Please disconnect first.');\n }\n\n // Save config for potential reconnection\n this.lastConnectionConfig = config;\n this.connectStartTime = performance.now();\n\n trackEvent('rtc_connect_start', 'info', {\n provider: this.providerName,\n });\n\n try {\n // Setup animation callbacks before connecting\n await this.setupAnimationCallbacks();\n await this.provider.connect(config);\n this._isConnected = true;\n this.sessionStartTime = Date.now();\n this.stallCount = 0;\n this.reconnectCount = 0;\n this.conversationCount = 0;\n\n trackEvent('rtc_connect_success', 'info', {\n provider: this.providerName,\n duration: Math.round(performance.now() - this.connectStartTime),\n });\n } catch (error) {\n trackEvent('rtc_connect_failed', 'error', {\n provider: this.providerName,\n reason: error instanceof Error ? error.message : String(error),\n duration: Math.round(performance.now() - this.connectStartTime),\n });\n throw error;\n }\n }\n\n /**\n * Pre-warm a future RTC connection when the provider supports it.\n * This is best-effort and does not change the connected state.\n */\n async prepareConnection(config: RTCPrepareConnectionConfig): Promise<void> {\n await this.provider.prepareConnection?.(config);\n }\n\n /**\n * Disconnect from RTC server.\n * Stops all tracks and cleans up resources.\n */\n async disconnect(): Promise<void> {\n if (!this._isConnected) {\n return;\n }\n\n // Stop microphone if active\n if (this.microphoneStream) {\n await this.stopPublishing();\n } else if (this.localAudioTrack) {\n // Stop custom audio track\n await this.unpublishAudio();\n }\n\n // Collect session summary before teardown\n const sessionSummary = this.animationHandler.getSessionSummary();\n\n await this.provider.disconnect();\n this.animationHandler.dispose();\n this._isConnected = false;\n\n const sessionDuration = this.sessionStartTime ? Date.now() - this.sessionStartTime : 0;\n\n trackEvent('rtc_session_summary', 'info', {\n provider: this.providerName,\n total_duration_ms: sessionDuration,\n total_frames: sessionSummary.totalFrames,\n total_lost: sessionSummary.totalLost,\n total_recovered: sessionSummary.totalRecovered,\n total_dropped: sessionSummary.totalDropped,\n avg_fps: sessionSummary.avgFps,\n stall_count: this.stallCount,\n reconnect_count: this.reconnectCount,\n conversation_count: this.conversationCount,\n });\n\n trackEvent('rtc_disconnected', 'info', {\n provider: this.providerName,\n session_duration: sessionDuration,\n });\n\n flushTelemetry();\n }\n\n /**\n * Publish an audio track to the RTC server.\n *\n * The audio source is controlled externally - you can pass any audio track:\n * - Microphone: `getUserMedia({ audio: true })`\n * - Browser audio: `audioElement.captureStream()`\n * - Web Audio API: `audioContext.createMediaStreamDestination().stream`\n * - Screen share audio: `getDisplayMedia({ audio: true })`\n *\n * @param track - MediaStreamTrack to publish (must be an audio track)\n * @throws Error if not connected or track is invalid\n *\n * @example\n * ```typescript\n * // Microphone\n * const stream = await navigator.mediaDevices.getUserMedia({ audio: true });\n * await player.publishAudio(stream.getAudioTracks()[0]);\n *\n * // Browser audio element\n * const audioEl = document.querySelector('audio');\n * const stream = audioEl.captureStream();\n * await player.publishAudio(stream.getAudioTracks()[0]);\n * ```\n */\n async publishAudio(track: MediaStreamTrack): Promise<void> {\n if (!this._isConnected) {\n throw new Error('Not connected. Please call connect() first.');\n }\n\n if (track.kind !== 'audio') {\n throw new Error('Invalid track: expected audio track');\n }\n\n // Unpublish existing track if any\n if (this.localAudioTrack) {\n await this.unpublishAudio();\n }\n\n this.localAudioTrack = track;\n try {\n await this.provider.publishAudioTrack(track);\n } catch (error) {\n this.localAudioTrack = null;\n trackEvent('rtc_audio_publish_failed', 'error', {\n provider: this.providerName,\n description: error instanceof Error ? error.message : String(error),\n });\n throw error;\n }\n }\n\n /**\n * Stop publishing audio.\n * Note: This does NOT stop the track - the caller is responsible for managing the track lifecycle.\n */\n async unpublishAudio(): Promise<void> {\n if (!this.localAudioTrack) {\n return;\n }\n\n await this.provider.unpublishAudioTrack();\n this.localAudioTrack = null;\n }\n\n /**\n * Start publishing microphone audio.\n *\n * This requests microphone permission and publishes it to the RTC session.\n * For custom audio sources (e.g., audio files), use `publishAudio(track)` instead.\n *\n * @throws Error if not connected, permission denied, or no microphone found\n *\n * @example\n * ```typescript\n * await player.startPublishing();\n * // ... later\n * await player.stopPublishing();\n * ```\n */\n async startPublishing(): Promise<void> {\n if (!this._isConnected) {\n throw new Error('Not connected. Please call connect() first.');\n }\n\n if (this.microphoneStream) {\n return; // Already using microphone\n }\n\n // Request microphone permission\n this.microphoneStream = await navigator.mediaDevices.getUserMedia({\n audio: true,\n });\n const track = this.microphoneStream.getAudioTracks()[0];\n\n await this.publishAudio(track);\n }\n\n /**\n * Stop publishing microphone audio and release the microphone.\n *\n * This stops the microphone track and releases the device.\n * If you used `publishAudio()` with a custom track, use `unpublishAudio()` instead.\n */\n async stopPublishing(): Promise<void> {\n if (!this.microphoneStream) {\n return;\n }\n\n await this.unpublishAudio();\n\n // Stop and release microphone\n this.microphoneStream.getTracks().forEach((track) => track.stop());\n this.microphoneStream = null;\n }\n\n /**\n * Get current connection state.\n * @returns Connection state string (e.g., 'connected', 'disconnected')\n */\n getConnectionState(): string {\n return this.provider.getConnectionState();\n }\n\n /**\n * Get the native RTC client object.\n *\n * Returns the underlying RTC client for advanced use cases:\n * - LiveKit: Returns the Room instance\n * - Agora: Returns the IAgoraRTCClient instance\n *\n * @returns The native RTC client object, or null if not connected\n *\n * @example\n * ```typescript\n * // Access LiveKit Room\n * const room = player.getNativeClient() as Room;\n * console.log('Participants:', room?.remoteParticipants.size);\n *\n * // Access Agora Client\n * const client = player.getNativeClient() as IAgoraRTCClient;\n * console.log('State:', client?.connectionState);\n * ```\n */\n getNativeClient(): unknown {\n return this.provider.getNativeClient();\n }\n\n /**\n * Add event listener.\n * @param event - Event name ('connected', 'disconnected', 'error', 'stalled', etc.)\n * @param handler - Event handler function\n */\n on(event: string, handler: (...args: unknown[]) => void): void {\n if (event === 'stalled') {\n // Handle stalled event locally\n if (!this.eventHandlers.has(event)) {\n this.eventHandlers.set(event, new Set());\n }\n this.eventHandlers.get(event)!.add(handler);\n } else {\n // Forward other events to provider\n this.provider.on(event, handler);\n }\n }\n\n /**\n * Remove event listener.\n * @param event - Event name\n * @param handler - Event handler function (must be same reference as added)\n */\n off(event: string, handler: (...args: unknown[]) => void): void {\n if (event === 'stalled') {\n this.eventHandlers.get(event)?.delete(handler);\n } else {\n this.provider.off(event, handler);\n }\n }\n\n /**\n * Emit an event to local listeners.\n * @internal\n */\n private emit(event: string, ...args: unknown[]): void {\n const handlers = this.eventHandlers.get(event);\n if (handlers) {\n handlers.forEach((handler) => {\n try {\n handler(...args);\n } catch (e) {\n logger.error('AvatarPlayer', `Error in ${event} handler:`, e);\n }\n });\n }\n }\n\n /**\n * Reconnect to RTC server using the last connection configuration.\n * Useful for recovering from connection issues or stream stalls.\n *\n * @example\n * ```typescript\n * player.on('stalled', async () => {\n * console.log('Stream stalled, attempting reconnection...');\n * await player.reconnect();\n * });\n * ```\n *\n * @throws Error if no previous connection config exists or reconnection fails\n */\n async reconnect(): Promise<void> {\n if (this.isReconnecting) {\n logger.warn('AvatarPlayer', 'Already attempting reconnection, skipping');\n return;\n }\n\n if (!this.lastConnectionConfig) {\n throw new Error(\n 'Cannot reconnect: no previous connection. Call connect() first.',\n );\n }\n\n this.isReconnecting = true;\n this.reconnectCount++;\n logger.info('AvatarPlayer', 'Attempting reconnection...');\n\n const reconnectStart = performance.now();\n trackEvent('rtc_reconnect_start', 'info', { provider: this.providerName });\n\n try {\n // Disconnect first if connected\n if (this._isConnected) {\n await this.disconnect();\n }\n\n // Wait a short moment before reconnecting\n await new Promise((resolve) => setTimeout(resolve, 500));\n\n // Reconnect\n await this.connect(this.lastConnectionConfig);\n logger.info('AvatarPlayer', 'Reconnection successful');\n\n trackEvent('rtc_reconnect_success', 'info', {\n provider: this.providerName,\n duration: Math.round(performance.now() - reconnectStart),\n });\n } catch (error) {\n logger.error('AvatarPlayer', 'Reconnection failed:', error);\n trackEvent('rtc_reconnect_failed', 'error', {\n provider: this.providerName,\n reason: error instanceof Error ? error.message : String(error),\n });\n throw error;\n } finally {\n this.isReconnecting = false;\n }\n }\n\n /**\n * Create internal renderer adapter that bridges AnimationHandler to AvatarView.\n * @internal\n */\n private createRendererAdapter() {\n const avatarView = this.avatarView;\n\n return {\n renderFromProtobuf(protobufData: ArrayBuffer): void {\n avatarView.renderFromProtobuf(protobufData);\n },\n\n renderFrame(frame: unknown, startIdle?: boolean): void {\n const view = avatarView as any;\n if (startIdle) {\n view.renderFlame(undefined, true);\n } else if (frame) {\n view.renderFlame(frame);\n }\n },\n\n async generateTransitionFromProtobuf(\n protobufData: ArrayBuffer,\n frameCount: number,\n ): Promise<unknown[]> {\n return avatarView.generateTransitionFromProtobuf(protobufData, frameCount);\n },\n\n isReady(): boolean {\n return true;\n },\n };\n }\n\n /**\n * Setup animation track callbacks.\n * @internal\n */\n private async setupAnimationCallbacks(): Promise<void> {\n const callbacks: AnimationTrackCallbacks = {\n onAnimationData: (protobufData, metadata) => {\n // Reset tracking only once per session to avoid duplicate start-packet resets.\n if (!this.hasActiveAnimationSession) {\n if (!this.isTrackingPrimed) {\n this.animationHandler.resetTracking();\n }\n\n this.hasActiveAnimationSession = true;\n this.isTrackingPrimed = false;\n\n if (!metadata.isStart) {\n logger.info(\n 'AvatarPlayer',\n `Session start inferred from frame seq=${metadata.frameSeq ?? 'n/a'}`,\n );\n }\n } else if (metadata.isStart) {\n logger.info(\n 'AvatarPlayer',\n `Ignoring duplicate session start frame seq=${metadata.frameSeq ?? 'n/a'}`,\n );\n }\n\n this.animationHandler.handleAnimationData(\n protobufData,\n metadata.frameSeq,\n metadata.isRecovered,\n );\n },\n onTransition: (protobufData, transitionFrameCount) => {\n this.conversationCount++;\n this.animationHandler.handleTransitionData(\n protobufData,\n transitionFrameCount,\n );\n },\n onTransitionEnd: (protobufData, transitionFrameCount) => {\n this.animationHandler.handleTransitionToIdle(\n protobufData,\n transitionFrameCount,\n );\n },\n onSessionStart: () => {\n // Session started - can be used for UI feedback\n },\n onSessionEnd: () => {\n // Session ended - can be used for UI feedback\n },\n onIdleStart: () => {\n // Idle packets can race with start-transition packets; avoid rendering idle mid-transition.\n if (this.animationHandler.isInTransition()) {\n // Keep session bookkeeping in sync even if visual idle is deferred.\n this.hasActiveAnimationSession = false;\n this.isTrackingPrimed = false;\n logger.info(\n 'AvatarPlayer',\n 'Deferring idleStart while transition is active',\n );\n return;\n }\n\n this.hasActiveAnimationSession = false;\n this.animationHandler.resetTracking();\n this.isTrackingPrimed = true;\n this.animationHandler.startIdle();\n },\n onStreamStats: (stats) => {\n // Log packet loss/recovery stats\n if (\n stats.framesLost > 0 ||\n stats.framesRecovered > 0 ||\n stats.framesDropped > 0\n ) {\n logger.warn(\n 'AvatarPlayer',\n `Stream stats: lost=${stats.framesLost}, recovered=${stats.framesRecovered}, dropped=${stats.framesDropped}, fps=${stats.framesPerSec}`,\n );\n }\n },\n };\n\n await this.provider.subscribeAnimationTrack(callbacks);\n }\n\n /**\n * Setup provider event handlers.\n * @internal\n */\n private setupProviderEvents(): void {\n this.provider.on('connected', () => {\n this._isConnected = true;\n });\n\n this.provider.on('disconnected', () => {\n this._isConnected = false;\n this.hasActiveAnimationSession = false;\n this.isTrackingPrimed = false;\n // Return to idle animation when disconnected\n this.animationHandler.startIdle();\n });\n\n this.provider.on('error', (error: unknown) => {\n trackEvent('rtc_error', 'error', {\n provider: this.providerName,\n description: error instanceof Error ? error.message : String(error),\n });\n });\n }\n}\n"],"names":[],"mappings":"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"}
|
package/dist/index3.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
2
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
3
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
-
import { BaseProvider } from "./
|
|
5
|
-
import { isLiveKitConfig } from "./index5.js";
|
|
6
|
-
import { VP8Extractor } from "./
|
|
7
|
-
import { getInsertableStreamsMethod } from "./
|
|
4
|
+
import { BaseProvider } from "./index9.js";
|
|
5
|
+
import { isLiveKitPrepareConfig, isLiveKitConfig } from "./index5.js";
|
|
6
|
+
import { VP8Extractor } from "./index10.js";
|
|
7
|
+
import { getInsertableStreamsMethod } from "./index11.js";
|
|
8
8
|
import { logger } from "./index7.js";
|
|
9
9
|
const globalLiveKitProviderRegistry = /* @__PURE__ */ new Set();
|
|
10
10
|
let rtcPeerConnectionPatched = false;
|
|
@@ -29,11 +29,19 @@ function patchRTCPeerConnection() {
|
|
|
29
29
|
listener.handleEvent(event);
|
|
30
30
|
}
|
|
31
31
|
};
|
|
32
|
-
return originalAddEventListener.call(
|
|
32
|
+
return originalAddEventListener.call(
|
|
33
|
+
this,
|
|
34
|
+
type,
|
|
35
|
+
wrappedListener,
|
|
36
|
+
options
|
|
37
|
+
);
|
|
33
38
|
}
|
|
34
39
|
return originalAddEventListener.call(this, type, listener, options);
|
|
35
40
|
};
|
|
36
|
-
const originalOnTrackDescriptor = Object.getOwnPropertyDescriptor(
|
|
41
|
+
const originalOnTrackDescriptor = Object.getOwnPropertyDescriptor(
|
|
42
|
+
RTCPeerConnection.prototype,
|
|
43
|
+
"ontrack"
|
|
44
|
+
);
|
|
37
45
|
if (originalOnTrackDescriptor) {
|
|
38
46
|
Object.defineProperty(RTCPeerConnection.prototype, "ontrack", {
|
|
39
47
|
get: originalOnTrackDescriptor.get,
|
|
@@ -85,6 +93,18 @@ class LiveKitProvider extends BaseProvider {
|
|
|
85
93
|
__publicField(this, "audioCallbacks", null);
|
|
86
94
|
/** @internal */
|
|
87
95
|
__publicField(this, "audioElements", /* @__PURE__ */ new Map());
|
|
96
|
+
/** @internal */
|
|
97
|
+
__publicField(this, "awaitingAudioUnlock", false);
|
|
98
|
+
/** @internal */
|
|
99
|
+
__publicField(this, "audioUnlockEventNames", [
|
|
100
|
+
"pointerdown",
|
|
101
|
+
"keydown",
|
|
102
|
+
"touchstart"
|
|
103
|
+
]);
|
|
104
|
+
/** @internal */
|
|
105
|
+
__publicField(this, "handleAudioUnlockGesture", () => {
|
|
106
|
+
void this.ensureAudioPlaybackUnlocked("user-gesture");
|
|
107
|
+
});
|
|
88
108
|
// Microphone publishing
|
|
89
109
|
/** @internal */
|
|
90
110
|
__publicField(this, "localAudioTrack", null);
|
|
@@ -92,6 +112,13 @@ class LiveKitProvider extends BaseProvider {
|
|
|
92
112
|
__publicField(this, "isMicrophoneEnabled", false);
|
|
93
113
|
globalLiveKitProviderRegistry.add(this);
|
|
94
114
|
}
|
|
115
|
+
createRoom(livekit) {
|
|
116
|
+
const { Room } = livekit;
|
|
117
|
+
return new Room({
|
|
118
|
+
videoCaptureDefaults: void 0,
|
|
119
|
+
audioCaptureDefaults: void 0
|
|
120
|
+
});
|
|
121
|
+
}
|
|
95
122
|
/**
|
|
96
123
|
* Load LiveKit SDK dynamically.
|
|
97
124
|
* @internal
|
|
@@ -103,12 +130,31 @@ class LiveKitProvider extends BaseProvider {
|
|
|
103
130
|
try {
|
|
104
131
|
this.livekitSDK = await import("livekit-client");
|
|
105
132
|
return this.livekitSDK;
|
|
106
|
-
} catch
|
|
133
|
+
} catch {
|
|
107
134
|
throw new Error(
|
|
108
135
|
"❌ Failed to load livekit-client.\nPlease ensure it is installed: pnpm add livekit-client"
|
|
109
136
|
);
|
|
110
137
|
}
|
|
111
138
|
}
|
|
139
|
+
async prepareConnection(config) {
|
|
140
|
+
logger.info("LiveKit", "prepareConnection() called with config:", {
|
|
141
|
+
hasUrl: "url" in config,
|
|
142
|
+
hasToken: "token" in config
|
|
143
|
+
});
|
|
144
|
+
if (!isLiveKitPrepareConfig(config)) {
|
|
145
|
+
throw new Error("LiveKitProvider requires url in prepareConnection config");
|
|
146
|
+
}
|
|
147
|
+
const livekit = await this.loadSDK();
|
|
148
|
+
if (!this.room) {
|
|
149
|
+
this.room = this.createRoom(livekit);
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
await this.room.prepareConnection(config.url, config.token);
|
|
153
|
+
logger.info("LiveKit", "prepareConnection() completed");
|
|
154
|
+
} catch (error) {
|
|
155
|
+
logger.warn("LiveKit", "prepareConnection() failed (non-fatal):", error);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
112
158
|
/**
|
|
113
159
|
* Map LiveKit connection state to string.
|
|
114
160
|
* @internal
|
|
@@ -269,6 +315,117 @@ class LiveKitProvider extends BaseProvider {
|
|
|
269
315
|
this.vp8Extractors.delete(receiver);
|
|
270
316
|
}
|
|
271
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* Add one-time user gesture listeners to recover from autoplay blocking.
|
|
320
|
+
* @internal
|
|
321
|
+
*/
|
|
322
|
+
addAudioUnlockListeners() {
|
|
323
|
+
if (typeof window === "undefined" || this.awaitingAudioUnlock) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
this.awaitingAudioUnlock = true;
|
|
327
|
+
for (const eventName of this.audioUnlockEventNames) {
|
|
328
|
+
window.addEventListener(eventName, this.handleAudioUnlockGesture, {
|
|
329
|
+
capture: true,
|
|
330
|
+
passive: true
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
logger.warn(
|
|
334
|
+
"LiveKit",
|
|
335
|
+
"Audio playback blocked by autoplay policy, waiting for user interaction"
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Remove user gesture listeners after playback is unlocked.
|
|
340
|
+
* @internal
|
|
341
|
+
*/
|
|
342
|
+
removeAudioUnlockListeners() {
|
|
343
|
+
if (typeof window === "undefined" || !this.awaitingAudioUnlock) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
this.awaitingAudioUnlock = false;
|
|
347
|
+
for (const eventName of this.audioUnlockEventNames) {
|
|
348
|
+
window.removeEventListener(
|
|
349
|
+
eventName,
|
|
350
|
+
this.handleAudioUnlockGesture,
|
|
351
|
+
true
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Detect browser autoplay blocking errors from audio play/start calls.
|
|
357
|
+
* @internal
|
|
358
|
+
*/
|
|
359
|
+
isAutoplayBlockedError(error) {
|
|
360
|
+
if (typeof DOMException !== "undefined" && error instanceof DOMException) {
|
|
361
|
+
return error.name === "NotAllowedError";
|
|
362
|
+
}
|
|
363
|
+
if (error instanceof Error) {
|
|
364
|
+
return error.name === "NotAllowedError" || /didn't interact|user didn't interact|NotAllowedError/i.test(
|
|
365
|
+
error.message
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Try to resume all attached remote audio elements.
|
|
372
|
+
* @returns true if at least one element is still blocked by autoplay policy
|
|
373
|
+
* @internal
|
|
374
|
+
*/
|
|
375
|
+
async resumeAttachedAudioElements(reason) {
|
|
376
|
+
let hasAutoplayBlockedElement = false;
|
|
377
|
+
for (const [participantIdentity, audioElement] of this.audioElements) {
|
|
378
|
+
if (!audioElement.paused) {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
await audioElement.play();
|
|
383
|
+
} catch (error) {
|
|
384
|
+
if (this.isAutoplayBlockedError(error)) {
|
|
385
|
+
hasAutoplayBlockedElement = true;
|
|
386
|
+
}
|
|
387
|
+
logger.warn(
|
|
388
|
+
"LiveKit",
|
|
389
|
+
`Failed to resume remote audio element (${reason}) for ${participantIdentity}:`,
|
|
390
|
+
error
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return hasAutoplayBlockedElement;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Ensure remote audio playback is unlocked and resumed.
|
|
398
|
+
* @internal
|
|
399
|
+
*/
|
|
400
|
+
async ensureAudioPlaybackUnlocked(reason) {
|
|
401
|
+
if (!this.room) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const roomWithAudioControl = this.room;
|
|
405
|
+
if (!roomWithAudioControl.canPlaybackAudio && typeof roomWithAudioControl.startAudio === "function") {
|
|
406
|
+
try {
|
|
407
|
+
await roomWithAudioControl.startAudio();
|
|
408
|
+
logger.info("LiveKit", `Audio context resumed (${reason})`);
|
|
409
|
+
} catch (error) {
|
|
410
|
+
if (this.isAutoplayBlockedError(error)) {
|
|
411
|
+
this.addAudioUnlockListeners();
|
|
412
|
+
} else {
|
|
413
|
+
logger.warn(
|
|
414
|
+
"LiveKit",
|
|
415
|
+
`Failed to call room.startAudio() (${reason}):`,
|
|
416
|
+
error
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const hasAutoplayBlockedElement = await this.resumeAttachedAudioElements(reason);
|
|
423
|
+
if (roomWithAudioControl.canPlaybackAudio && !hasAutoplayBlockedElement) {
|
|
424
|
+
this.removeAudioUnlockListeners();
|
|
425
|
+
} else {
|
|
426
|
+
this.addAudioUnlockListeners();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
272
429
|
async connect(config) {
|
|
273
430
|
var _a;
|
|
274
431
|
logger.info("LiveKit", "connect() called with config:", {
|
|
@@ -278,18 +435,26 @@ class LiveKitProvider extends BaseProvider {
|
|
|
278
435
|
config
|
|
279
436
|
});
|
|
280
437
|
if (!isLiveKitConfig(config)) {
|
|
281
|
-
logger.error(
|
|
282
|
-
|
|
438
|
+
logger.error(
|
|
439
|
+
"LiveKit",
|
|
440
|
+
"Config validation failed - missing url or token"
|
|
441
|
+
);
|
|
442
|
+
throw new Error(
|
|
443
|
+
"LiveKitProvider requires url and token in connection config"
|
|
444
|
+
);
|
|
283
445
|
}
|
|
284
446
|
const livekitConfig = config;
|
|
285
|
-
logger.info(
|
|
447
|
+
logger.info(
|
|
448
|
+
"LiveKit",
|
|
449
|
+
"Config validated, connecting to:",
|
|
450
|
+
livekitConfig.url
|
|
451
|
+
);
|
|
452
|
+
this.removeAudioUnlockListeners();
|
|
286
453
|
const livekit = await this.loadSDK();
|
|
287
454
|
logger.info("LiveKit", "SDK loaded successfully");
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
audioCaptureDefaults: void 0
|
|
292
|
-
});
|
|
455
|
+
if (!this.room) {
|
|
456
|
+
this.room = this.createRoom(livekit);
|
|
457
|
+
}
|
|
293
458
|
this.setConnectionState("connecting");
|
|
294
459
|
this.setupEventListeners(livekit);
|
|
295
460
|
try {
|
|
@@ -297,16 +462,31 @@ class LiveKitProvider extends BaseProvider {
|
|
|
297
462
|
await this.room.connect(livekitConfig.url, livekitConfig.token);
|
|
298
463
|
logger.info("LiveKit", "Room connected, state:", this.room.state);
|
|
299
464
|
logger.info("LiveKit", "Room name:", this.room.name);
|
|
300
|
-
logger.info(
|
|
301
|
-
|
|
465
|
+
logger.info(
|
|
466
|
+
"LiveKit",
|
|
467
|
+
"Local participant:",
|
|
468
|
+
(_a = this.room.localParticipant) == null ? void 0 : _a.identity
|
|
469
|
+
);
|
|
470
|
+
logger.info(
|
|
471
|
+
"LiveKit",
|
|
472
|
+
"Remote participants:",
|
|
473
|
+
this.room.remoteParticipants.size
|
|
474
|
+
);
|
|
302
475
|
this.room.remoteParticipants.forEach((participant, sid) => {
|
|
303
|
-
logger.info(
|
|
476
|
+
logger.info(
|
|
477
|
+
"LiveKit",
|
|
478
|
+
`Remote participant: ${participant.identity}, sid: ${sid}`
|
|
479
|
+
);
|
|
304
480
|
participant.trackPublications.forEach((pub, trackSid) => {
|
|
305
|
-
logger.info(
|
|
481
|
+
logger.info(
|
|
482
|
+
"LiveKit",
|
|
483
|
+
` Track: ${pub.trackName}, kind: ${pub.kind}, subscribed: ${pub.isSubscribed}, sid: ${trackSid}`
|
|
484
|
+
);
|
|
306
485
|
});
|
|
307
486
|
});
|
|
308
|
-
this.setConnectionState(
|
|
309
|
-
|
|
487
|
+
this.setConnectionState(
|
|
488
|
+
this.mapLiveKitConnectionState(livekit, this.room.state)
|
|
489
|
+
);
|
|
310
490
|
} catch (error) {
|
|
311
491
|
logger.error("LiveKit", "Connection failed:", error);
|
|
312
492
|
this.setConnectionState("failed");
|
|
@@ -325,9 +505,19 @@ class LiveKitProvider extends BaseProvider {
|
|
|
325
505
|
room.on(RoomEvent.Connected, () => {
|
|
326
506
|
this.setConnectionState("connected");
|
|
327
507
|
this.emit("connected");
|
|
508
|
+
const roomWithAudioControl = room;
|
|
509
|
+
if (!roomWithAudioControl.canPlaybackAudio) {
|
|
510
|
+
this.addAudioUnlockListeners();
|
|
511
|
+
}
|
|
328
512
|
});
|
|
329
513
|
room.on(RoomEvent.ParticipantConnected, (participant) => {
|
|
330
|
-
logger.info("LiveKit", `
|
|
514
|
+
logger.info("LiveKit", `Participant connected: ${participant.identity}`);
|
|
515
|
+
});
|
|
516
|
+
room.on(RoomEvent.ParticipantDisconnected, (participant) => {
|
|
517
|
+
logger.warn(
|
|
518
|
+
"LiveKit",
|
|
519
|
+
`Participant disconnected: ${participant.identity}`
|
|
520
|
+
);
|
|
331
521
|
});
|
|
332
522
|
room.on(RoomEvent.TrackPublished, (publication, participant) => {
|
|
333
523
|
logger.info("LiveKit", "TrackPublished:", {
|
|
@@ -341,6 +531,22 @@ class LiveKitProvider extends BaseProvider {
|
|
|
341
531
|
this.setConnectionState("disconnected");
|
|
342
532
|
this.emit("disconnected");
|
|
343
533
|
});
|
|
534
|
+
room.on(
|
|
535
|
+
RoomEvent.AudioPlaybackStatusChanged,
|
|
536
|
+
(canPlaybackAudio) => {
|
|
537
|
+
logger.info(
|
|
538
|
+
"LiveKit",
|
|
539
|
+
`Audio playback status changed: ${canPlaybackAudio ? "enabled" : "blocked"}`
|
|
540
|
+
);
|
|
541
|
+
if (canPlaybackAudio) {
|
|
542
|
+
void this.ensureAudioPlaybackUnlocked(
|
|
543
|
+
"audio-playback-status-changed"
|
|
544
|
+
);
|
|
545
|
+
} else {
|
|
546
|
+
this.addAudioUnlockListeners();
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
);
|
|
344
550
|
this.room.on(
|
|
345
551
|
RoomEvent.TrackSubscribed,
|
|
346
552
|
(track, publication, participant) => {
|
|
@@ -359,22 +565,40 @@ class LiveKitProvider extends BaseProvider {
|
|
|
359
565
|
audioElement.autoplay = true;
|
|
360
566
|
audioElement.controls = false;
|
|
361
567
|
audioElement.style.display = "none";
|
|
568
|
+
const previousAudioElement = this.audioElements.get(
|
|
569
|
+
participant.identity
|
|
570
|
+
);
|
|
571
|
+
if (previousAudioElement && previousAudioElement !== audioElement) {
|
|
572
|
+
previousAudioElement.remove();
|
|
573
|
+
}
|
|
362
574
|
if (typeof document !== "undefined") {
|
|
363
575
|
document.body.appendChild(audioElement);
|
|
364
576
|
logger.info("LiveKit", "Audio element appended to body");
|
|
365
577
|
}
|
|
366
578
|
this.audioElements.set(participant.identity, audioElement);
|
|
579
|
+
void this.ensureAudioPlaybackUnlocked("audio-track-subscribed");
|
|
367
580
|
if (this.audioCallbacks) {
|
|
368
581
|
(_b = (_a = this.audioCallbacks).onAudioReceived) == null ? void 0 : _b.call(_a, participant);
|
|
369
582
|
}
|
|
370
583
|
} else if (track.kind === Track.Kind.Video) {
|
|
371
|
-
logger.info(
|
|
584
|
+
logger.info(
|
|
585
|
+
"LiveKit",
|
|
586
|
+
"Video track received, setting up animation transform..."
|
|
587
|
+
);
|
|
372
588
|
if (mediaStreamTrack) {
|
|
373
|
-
const receiver = this.findVideoReceiverByTrackId(
|
|
589
|
+
const receiver = this.findVideoReceiverByTrackId(
|
|
590
|
+
mediaStreamTrack.id
|
|
591
|
+
);
|
|
374
592
|
if (receiver) {
|
|
375
|
-
this.receiverParticipantMap.set(receiver, {
|
|
593
|
+
this.receiverParticipantMap.set(receiver, {
|
|
594
|
+
participant,
|
|
595
|
+
trackName
|
|
596
|
+
});
|
|
376
597
|
if (!receiver.transform || !this.transformedReceivers.has(receiver)) {
|
|
377
|
-
this.applyAnimationReceiverTransform(
|
|
598
|
+
this.applyAnimationReceiverTransform(
|
|
599
|
+
receiver,
|
|
600
|
+
mediaStreamTrack
|
|
601
|
+
);
|
|
378
602
|
}
|
|
379
603
|
}
|
|
380
604
|
}
|
|
@@ -423,6 +647,7 @@ class LiveKitProvider extends BaseProvider {
|
|
|
423
647
|
}
|
|
424
648
|
return this.connectionState;
|
|
425
649
|
}
|
|
650
|
+
/** @internal */
|
|
426
651
|
async subscribeAnimationTrack(callbacks) {
|
|
427
652
|
var _a;
|
|
428
653
|
this.animationCallbacks = callbacks;
|
|
@@ -439,9 +664,14 @@ class LiveKitProvider extends BaseProvider {
|
|
|
439
664
|
if (((_a = publication.trackName) == null ? void 0 : _a.includes("animation")) && publication.track) {
|
|
440
665
|
const mediaStreamTrack = publication.track.mediaStreamTrack;
|
|
441
666
|
if (mediaStreamTrack) {
|
|
442
|
-
const receiver = this.findVideoReceiverByTrackId(
|
|
667
|
+
const receiver = this.findVideoReceiverByTrackId(
|
|
668
|
+
mediaStreamTrack.id
|
|
669
|
+
);
|
|
443
670
|
if (receiver) {
|
|
444
|
-
this.applyAnimationReceiverTransform(
|
|
671
|
+
this.applyAnimationReceiverTransform(
|
|
672
|
+
receiver,
|
|
673
|
+
mediaStreamTrack
|
|
674
|
+
);
|
|
445
675
|
}
|
|
446
676
|
}
|
|
447
677
|
}
|
|
@@ -451,6 +681,7 @@ class LiveKitProvider extends BaseProvider {
|
|
|
451
681
|
}
|
|
452
682
|
}
|
|
453
683
|
}
|
|
684
|
+
/** @internal */
|
|
454
685
|
async unsubscribeAnimationTrack() {
|
|
455
686
|
this.animationCallbacks = null;
|
|
456
687
|
this.vp8Extractors.forEach((extractor) => {
|
|
@@ -464,13 +695,14 @@ class LiveKitProvider extends BaseProvider {
|
|
|
464
695
|
this.transformedReceivers.clear();
|
|
465
696
|
this.receiverParticipantMap.clear();
|
|
466
697
|
}
|
|
698
|
+
/** @internal */
|
|
467
699
|
async subscribeAudioTrack(callbacks) {
|
|
468
700
|
this.audioCallbacks = callbacks;
|
|
469
701
|
}
|
|
702
|
+
/** @internal */
|
|
470
703
|
async unsubscribeAudioTrack() {
|
|
471
704
|
this.audioCallbacks = null;
|
|
472
705
|
this.audioElements.forEach((el) => {
|
|
473
|
-
el.pause();
|
|
474
706
|
el.remove();
|
|
475
707
|
});
|
|
476
708
|
this.audioElements.clear();
|
|
@@ -517,35 +749,14 @@ class LiveKitProvider extends BaseProvider {
|
|
|
517
749
|
this.localAudioTrack = null;
|
|
518
750
|
this.isMicrophoneEnabled = false;
|
|
519
751
|
}
|
|
520
|
-
/**
|
|
521
|
-
* Play remote audio (resume playback)
|
|
522
|
-
*/
|
|
523
|
-
playRemoteAudio() {
|
|
524
|
-
this.audioElements.forEach((audioElement) => {
|
|
525
|
-
if (audioElement.paused) {
|
|
526
|
-
audioElement.play().catch(() => {
|
|
527
|
-
});
|
|
528
|
-
}
|
|
529
|
-
});
|
|
530
|
-
}
|
|
531
|
-
/**
|
|
532
|
-
* Pause remote audio
|
|
533
|
-
*/
|
|
534
|
-
pauseRemoteAudio() {
|
|
535
|
-
this.audioElements.forEach((audioElement) => {
|
|
536
|
-
if (!audioElement.paused) {
|
|
537
|
-
audioElement.pause();
|
|
538
|
-
}
|
|
539
|
-
});
|
|
540
|
-
}
|
|
541
752
|
/**
|
|
542
753
|
* Get the native LiveKit Room instance.
|
|
543
|
-
*
|
|
754
|
+
*
|
|
544
755
|
* Allows advanced users to access LiveKit-specific features
|
|
545
756
|
* not exposed through the unified API.
|
|
546
|
-
*
|
|
757
|
+
*
|
|
547
758
|
* @returns The LiveKit Room instance, or null if not connected
|
|
548
|
-
*
|
|
759
|
+
*
|
|
549
760
|
* @example
|
|
550
761
|
* ```typescript
|
|
551
762
|
* const room = provider.getNativeClient();
|
|
@@ -563,8 +774,8 @@ class LiveKitProvider extends BaseProvider {
|
|
|
563
774
|
* @internal
|
|
564
775
|
*/
|
|
565
776
|
cleanup() {
|
|
777
|
+
this.removeAudioUnlockListeners();
|
|
566
778
|
this.audioElements.forEach((el) => {
|
|
567
|
-
el.pause();
|
|
568
779
|
el.remove();
|
|
569
780
|
});
|
|
570
781
|
this.audioElements.clear();
|