diff --git a/demo/app.tsx b/demo/app.tsx index aa2b19fc..4d10b3ac 100644 --- a/demo/app.tsx +++ b/demo/app.tsx @@ -13,7 +13,7 @@ export function App() { const [mode, setMode] = useState(ChatMode.Functional); const [sessionTimeout, setSessionTimeout] = useState(); const [compatibilityMode, setCompatibilityMode] = useState<'on' | 'off' | 'auto'>(); - const [fluent, setFluent] = useState(false); + const [fluent, setFluent] = useState(true); const [enableMicrophone, setEnableMicrophone] = useState(true); const [microphoneStream, setMicrophoneStream] = useState(undefined); const microphoneStreamRef = useRef(undefined); diff --git a/package.json b/package.json index f1488d46..de330d97 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@d-id/client-sdk", "private": false, - "version": "1.1.32", + "version": "1.1.33", "type": "module", "description": "d-id client sdk", "repository": { diff --git a/src/services/streaming-manager/advanced.test.ts b/src/services/streaming-manager/advanced.test.ts index a4db8e9a..b1b54d68 100644 --- a/src/services/streaming-manager/advanced.test.ts +++ b/src/services/streaming-manager/advanced.test.ts @@ -5,7 +5,7 @@ import { StreamApiFactory, StreamingAgentFactory, StreamingManagerOptionsFactory } from '../../test-utils/factories'; import { CreateStreamOptions, StreamType, StreamingManagerOptions } from '../../types/index'; -import { pollStats } from './stats/poll'; +import { createVideoStatsMonitor } from './stats/poll'; import { createParseDataChannelMessage, createWebRTCStreamingManager as createStreamingManager, @@ -15,9 +15,14 @@ import { const mockApi = StreamApiFactory.build(); jest.mock('../../api/streams', () => ({ createStreamApi: jest.fn(() => mockApi) })); -// Mock pollStats +// Mock createVideoStatsMonitor +const mockVideoStatsMonitor = { + start: jest.fn(), + stop: jest.fn(), + getReport: jest.fn(() => ({})), +}; jest.mock('./stats/poll', () => ({ - pollStats: jest.fn(() => 123), // mock interval id + createVideoStatsMonitor: jest.fn(() => mockVideoStatsMonitor), })); // Mock other dependencies as needed @@ -391,7 +396,7 @@ describe('Streaming Manager Advanced', () => { it('should handle connectivity state changes via pollStats', async () => { const manager = await createStreamingManager(agentId, agent, options); - expect(pollStats).toHaveBeenCalledWith( + expect(createVideoStatsMonitor).toHaveBeenCalledWith( expect.anything(), expect.anything(), expect.anything(), @@ -399,7 +404,7 @@ describe('Streaming Manager Advanced', () => { expect.any(Function) ); - const connectivityCallback = (pollStats as jest.Mock).mock.calls[0][4]; + const connectivityCallback = (createVideoStatsMonitor as jest.Mock).mock.calls[0][4]; connectivityCallback('test-connectivity-state'); expect(options.callbacks.onConnectivityStateChange).toHaveBeenCalledWith('test-connectivity-state'); @@ -408,7 +413,7 @@ describe('Streaming Manager Advanced', () => { it('should handle pollStats return value', async () => { const manager = await createStreamingManager(agentId, agent, options); - expect(pollStats).toHaveBeenCalled(); + expect(createVideoStatsMonitor).toHaveBeenCalled(); expect(manager.streamId).toBe('streamId'); }); @@ -416,7 +421,7 @@ describe('Streaming Manager Advanced', () => { it('should handle video stats interval management', async () => { const manager = await createStreamingManager(agentId, agent, options); - expect(pollStats).toHaveBeenCalledWith( + expect(createVideoStatsMonitor).toHaveBeenCalledWith( expect.anything(), expect.anything(), expect.anything(), @@ -430,7 +435,7 @@ describe('Streaming Manager Advanced', () => { it('should handle pollStats function execution and return', async () => { const manager = await createStreamingManager(agentId, agent, options); - expect(pollStats).toHaveBeenCalled(); + expect(createVideoStatsMonitor).toHaveBeenCalled(); expect(manager.streamId).toBe('streamId'); expect(manager.sessionId).toBe('sessionId'); }); diff --git a/src/services/streaming-manager/business-flows.test.ts b/src/services/streaming-manager/business-flows.test.ts index 0caf2f31..6c58be7f 100644 --- a/src/services/streaming-manager/business-flows.test.ts +++ b/src/services/streaming-manager/business-flows.test.ts @@ -11,9 +11,14 @@ import { createWebRTCStreamingManager as createStreamingManager } from './webrtc const mockApi = StreamApiFactory.build(); jest.mock('../../api/streams', () => ({ createStreamApi: jest.fn(() => mockApi) })); -// Mock pollStats +// Mock createVideoStatsMonitor +const mockVideoStatsMonitor = { + start: jest.fn(), + stop: jest.fn(), + getReport: jest.fn(() => ({})), +}; jest.mock('./stats/poll', () => ({ - pollStats: jest.fn(() => 123), // mock interval id + createVideoStatsMonitor: jest.fn(() => mockVideoStatsMonitor), })); // Mock other dependencies as needed diff --git a/src/services/streaming-manager/disconnect.test.ts b/src/services/streaming-manager/disconnect.test.ts index a9325f35..44c79fe1 100644 --- a/src/services/streaming-manager/disconnect.test.ts +++ b/src/services/streaming-manager/disconnect.test.ts @@ -11,9 +11,14 @@ import { createWebRTCStreamingManager as createStreamingManager } from './webrtc const mockApi = StreamApiFactory.build(); jest.mock('../../api/streams', () => ({ createStreamApi: jest.fn(() => mockApi) })); -// Mock pollStats +// Mock createVideoStatsMonitor +const mockVideoStatsMonitor = { + start: jest.fn(), + stop: jest.fn(), + getReport: jest.fn(() => ({})), +}; jest.mock('./stats/poll', () => ({ - pollStats: jest.fn(() => 123), // mock interval id + createVideoStatsMonitor: jest.fn(() => mockVideoStatsMonitor), })); // Mock other dependencies as needed @@ -93,18 +98,14 @@ describe('Streaming Manager Disconnect', () => { expect(mockApi.close).toHaveBeenCalledWith('streamId', 'sessionId'); }); - it('should handle clearInterval in disconnect', async () => { + it('should handle videoStatsMonitor.stop in disconnect', async () => { const manager = await createStreamingManager(agentId, agentStreamOptions, options); const mockPC = (window.RTCPeerConnection as any).mock.results[0].value; mockPC.iceConnectionState = 'connected'; - const clearIntervalSpy = jest.spyOn(global, 'clearInterval'); - await manager.disconnect(); - expect(clearIntervalSpy).toHaveBeenCalled(); - - clearIntervalSpy.mockRestore(); + expect(mockVideoStatsMonitor.stop).toHaveBeenCalled(); }); it('should handle agent activity state changes on disconnect', async () => { @@ -130,19 +131,15 @@ describe('Streaming Manager Disconnect', () => { expect(mockPC.ontrack).toBeNull(); }); - it('should handle disconnect cleanup and clearInterval', async () => { + it('should handle disconnect cleanup and videoStatsMonitor.stop', async () => { const manager = await createStreamingManager(agentId, agentStreamOptions, options); const mockPC = (window.RTCPeerConnection as any).mock.results[0].value; mockPC.iceConnectionState = 'connected'; - const clearIntervalSpy = jest.spyOn(global, 'clearInterval'); - await manager.disconnect(); - expect(clearIntervalSpy).toHaveBeenCalled(); + expect(mockVideoStatsMonitor.stop).toHaveBeenCalled(); expect(options.callbacks.onAgentActivityStateChange).toHaveBeenCalledWith(AgentActivityState.Idle); - - clearIntervalSpy.mockRestore(); }); it('should handle srcObject cleanup in disconnect', async () => { @@ -162,12 +159,11 @@ describe('Streaming Manager Disconnect', () => { mockPC.iceConnectionState = 'connected'; const closeSpy = jest.spyOn(mockPC, 'close'); - const clearIntervalSpy = jest.spyOn(global, 'clearInterval'); await manager.disconnect(); expect(closeSpy).toHaveBeenCalled(); - expect(clearIntervalSpy).toHaveBeenCalled(); + expect(mockVideoStatsMonitor.stop).toHaveBeenCalled(); expect(mockPC.oniceconnectionstatechange).toBeNull(); expect(mockPC.onnegotiationneeded).toBeNull(); expect(mockPC.onicecandidate).toBeNull(); @@ -175,7 +171,6 @@ describe('Streaming Manager Disconnect', () => { expect(options.callbacks.onAgentActivityStateChange).toHaveBeenCalledWith(AgentActivityState.Idle); closeSpy.mockRestore(); - clearIntervalSpy.mockRestore(); }); }); @@ -231,18 +226,14 @@ describe('Streaming Manager Disconnect', () => { expect(manager.streamId).toBe('streamId'); }); - it('should handle clearInterval on disconnect', async () => { + it('should handle videoStatsMonitor.stop on disconnect', async () => { const manager = await createStreamingManager(agentId, agentStreamOptions, options); const mockPC = (window.RTCPeerConnection as any).mock.results[0].value; mockPC.iceConnectionState = 'connected'; - const clearIntervalSpy = jest.spyOn(global, 'clearInterval'); - await manager.disconnect(); - expect(clearIntervalSpy).toHaveBeenCalled(); - - clearIntervalSpy.mockRestore(); + expect(mockVideoStatsMonitor.stop).toHaveBeenCalled(); }); it('should handle complete disconnect cleanup sequence', async () => { @@ -250,14 +241,10 @@ describe('Streaming Manager Disconnect', () => { const mockPC = (window.RTCPeerConnection as any).mock.results[0].value; mockPC.iceConnectionState = 'connected'; - const clearIntervalSpy = jest.spyOn(global, 'clearInterval'); - await manager.disconnect(); - expect(clearIntervalSpy).toHaveBeenCalled(); + expect(mockVideoStatsMonitor.stop).toHaveBeenCalled(); expect(options.callbacks.onAgentActivityStateChange).toHaveBeenCalledWith(AgentActivityState.Idle); - - clearIntervalSpy.mockRestore(); }); it('should execute all disconnect cleanup paths', async () => { @@ -265,14 +252,10 @@ describe('Streaming Manager Disconnect', () => { const mockPC = (window.RTCPeerConnection as any).mock.results[0].value; mockPC.iceConnectionState = 'connected'; - const clearIntervalSpy = jest.spyOn(global, 'clearInterval'); - await manager.disconnect(); - expect(clearIntervalSpy).toHaveBeenCalled(); + expect(mockVideoStatsMonitor.stop).toHaveBeenCalled(); expect(options.callbacks.onAgentActivityStateChange).toHaveBeenCalledWith(AgentActivityState.Idle); - - clearIntervalSpy.mockRestore(); }); }); diff --git a/src/services/streaming-manager/edge-cases.test.ts b/src/services/streaming-manager/edge-cases.test.ts index 633cb8f1..103a403b 100644 --- a/src/services/streaming-manager/edge-cases.test.ts +++ b/src/services/streaming-manager/edge-cases.test.ts @@ -21,9 +21,14 @@ import { const mockApi = StreamApiFactory.build(); jest.mock('../../api/streams', () => ({ createStreamApi: jest.fn(() => mockApi) })); -// Mock pollStats +// Mock createVideoStatsMonitor +const mockVideoStatsMonitor = { + start: jest.fn(), + stop: jest.fn(), + getReport: jest.fn(() => ({})), +}; jest.mock('./stats/poll', () => ({ - pollStats: jest.fn(() => 123), // mock interval id + createVideoStatsMonitor: jest.fn(() => mockVideoStatsMonitor), })); // Mock other dependencies as needed diff --git a/src/services/streaming-manager/factory.test.ts b/src/services/streaming-manager/factory.test.ts index 6211ce75..6e0a7aa7 100644 --- a/src/services/streaming-manager/factory.test.ts +++ b/src/services/streaming-manager/factory.test.ts @@ -6,7 +6,7 @@ import { StreamingManagerOptions, TransportProvider, } from '../../types'; -import { createStreamingManager, StreamApiVersion } from './factory'; +import { StreamApiVersion, createStreamingManager } from './factory'; const mockCreateWebRTCStreamingManager = jest.fn(); jest.mock('./webrtc-manager', () => ({ diff --git a/src/services/streaming-manager/livekit-manager.test.ts b/src/services/streaming-manager/livekit-manager.test.ts index a9ff9d6f..6525e26b 100644 --- a/src/services/streaming-manager/livekit-manager.test.ts +++ b/src/services/streaming-manager/livekit-manager.test.ts @@ -1,5 +1,11 @@ import { StreamingManagerOptionsFactory } from '../../test-utils/factories'; -import { AgentActivityState, CreateSessionV2Options, StreamEvents, StreamingManagerOptions } from '../../types/index'; +import { + AgentActivityState, + CreateSessionV2Options, + StreamEvents, + StreamingManagerOptions, + StreamingState, +} from '../../types/index'; import { createLiveKitStreamingManager } from './livekit-manager'; // Mock livekit-client @@ -68,6 +74,16 @@ jest.mock('../../api/streams/streamsApiV2', () => ({ jest.mock('../../config/environment', () => ({ didApiUrl: 'http://test-api.com' })); +// Mock createVideoStatsMonitor +const mockVideoStatsMonitor = { + start: jest.fn(), + stop: jest.fn(), + getReport: jest.fn(() => ({})), +}; +jest.mock('./stats/poll', () => ({ + createVideoStatsMonitor: jest.fn(() => mockVideoStatsMonitor), +})); + const mockLatencyTimestampTrackerUpdate = jest.fn(); jest.mock('../analytics/timestamp-tracker', () => ({ latencyTimestampTracker: { @@ -139,6 +155,34 @@ function getTranscriptionReceivedHandler() { return calls.length > 0 ? calls[calls.length - 1][1] : undefined; } +function getTrackSubscribedHandler() { + const calls = mockRoom.on.mock.calls.filter((call: any[]) => call[0] === 'TrackSubscribed'); + return calls.length > 0 ? calls[calls.length - 1][1] : undefined; +} + +function getTrackUnsubscribedHandler() { + const calls = mockRoom.on.mock.calls.filter((call: any[]) => call[0] === 'TrackUnsubscribed'); + return calls.length > 0 ? calls[calls.length - 1][1] : undefined; +} + +function createMockVideoTrack() { + return { + kind: 'video', + mediaStreamTrack: { + kind: 'video', + id: 'video-track-1', + }, + getRTCStatsReport: jest.fn().mockResolvedValue(new Map()), + } as any; +} + +function createMockRemoteParticipant(identity: string = 'agent') { + return { + identity, + isLocal: false, + } as any; +} + async function simulateConnection(handlerIndex?: number) { const handler = getConnectionStateHandler(handlerIndex); if (handler) { @@ -155,6 +199,9 @@ describe('LiveKit Streaming Manager - Microphone Stream', () => { beforeEach(() => { jest.clearAllMocks(); mockLatencyTimestampTrackerUpdate.mockClear(); + mockVideoStatsMonitor.start.mockClear(); + mockVideoStatsMonitor.stop.mockClear(); + mockVideoStatsMonitor.getReport.mockClear(); agentId = TEST_AGENT_ID; sessionOptions = { chat_persist: true, @@ -457,4 +504,104 @@ describe('LiveKit Streaming Manager - Microphone Stream', () => { expect(mockOnInterruptDetected).not.toHaveBeenCalled(); }); }); + + describe('Video Stats Monitor', () => { + it('should start video stats monitor when video track is subscribed', async () => { + await createLiveKitStreamingManager(agentId, sessionOptions, options); + await simulateConnection(); + + const trackSubscribedHandler = getTrackSubscribedHandler(); + const mockVideoTrack = createMockVideoTrack(); + const mockParticipant = createMockRemoteParticipant(); + + trackSubscribedHandler(mockVideoTrack, {}, mockParticipant); + + expect(mockVideoStatsMonitor.start).toHaveBeenCalledTimes(1); + }); + + it('should not start video stats monitor for audio tracks', async () => { + await createLiveKitStreamingManager(agentId, sessionOptions, options); + await simulateConnection(); + + const trackSubscribedHandler = getTrackSubscribedHandler(); + const mockAudioTrack = createMockAudioTrack(); + (mockAudioTrack as any).mediaStreamTrack = mockAudioTrack; + const mockParticipant = createMockRemoteParticipant(); + + trackSubscribedHandler(mockAudioTrack, {}, mockParticipant); + + expect(mockVideoStatsMonitor.start).not.toHaveBeenCalled(); + }); + + it('should stop video stats monitor when video track is unsubscribed', async () => { + await createLiveKitStreamingManager(agentId, sessionOptions, options); + await simulateConnection(); + + const trackSubscribedHandler = getTrackSubscribedHandler(); + const trackUnsubscribedHandler = getTrackUnsubscribedHandler(); + const mockVideoTrack = createMockVideoTrack(); + const mockParticipant = createMockRemoteParticipant(); + + trackSubscribedHandler(mockVideoTrack, {}, mockParticipant); + expect(mockVideoStatsMonitor.start).toHaveBeenCalledTimes(1); + trackUnsubscribedHandler(mockVideoTrack, {}, mockParticipant); + + expect(mockVideoStatsMonitor.stop).toHaveBeenCalledTimes(1); + }); + + it('should get report from video stats monitor when video track is unsubscribed', async () => { + await createLiveKitStreamingManager(agentId, sessionOptions, options); + await simulateConnection(); + const mockReport = { duration: 1000 }; + mockVideoStatsMonitor.getReport.mockReturnValue(mockReport); + + const trackSubscribedHandler = getTrackSubscribedHandler(); + const trackUnsubscribedHandler = getTrackUnsubscribedHandler(); + const mockVideoTrack = createMockVideoTrack(); + const mockParticipant = createMockRemoteParticipant(); + + trackSubscribedHandler(mockVideoTrack, {}, mockParticipant); + trackUnsubscribedHandler(mockVideoTrack, {}, mockParticipant); + + expect(mockVideoStatsMonitor.getReport).toHaveBeenCalledTimes(1); + }); + + it('should call onVideoStateChange with Start when video track is subscribed', async () => { + const mockOnVideoStateChange = jest.fn(); + options.callbacks.onVideoStateChange = mockOnVideoStateChange; + + await createLiveKitStreamingManager(agentId, sessionOptions, options); + await simulateConnection(); + + const trackSubscribedHandler = getTrackSubscribedHandler(); + const mockVideoTrack = createMockVideoTrack(); + const mockParticipant = createMockRemoteParticipant(); + + trackSubscribedHandler(mockVideoTrack, {}, mockParticipant); + + expect(mockOnVideoStateChange).toHaveBeenCalledWith(StreamingState.Start); + }); + + it('should call onVideoStateChange with Stop and report when video track is unsubscribed', async () => { + const mockOnVideoStateChange = jest.fn(); + options.callbacks.onVideoStateChange = mockOnVideoStateChange; + const mockReport = { duration: 1000 }; + mockVideoStatsMonitor.getReport.mockReturnValue(mockReport); + + await createLiveKitStreamingManager(agentId, sessionOptions, options); + await simulateConnection(); + + const trackSubscribedHandler = getTrackSubscribedHandler(); + const trackUnsubscribedHandler = getTrackUnsubscribedHandler(); + const mockVideoTrack = createMockVideoTrack(); + const mockParticipant = createMockRemoteParticipant(); + + trackSubscribedHandler(mockVideoTrack, {}, mockParticipant); + mockOnVideoStateChange.mockClear(); + + trackUnsubscribedHandler(mockVideoTrack, {}, mockParticipant); + + expect(mockOnVideoStateChange).toHaveBeenCalledWith(StreamingState.Stop, mockReport); + }); + }); }); diff --git a/src/services/streaming-manager/livekit-manager.ts b/src/services/streaming-manager/livekit-manager.ts index 3cd6eaed..df0191f9 100644 --- a/src/services/streaming-manager/livekit-manager.ts +++ b/src/services/streaming-manager/livekit-manager.ts @@ -12,6 +12,7 @@ import { TransportProvider, } from '@sdk/types'; import { ChatProgress } from '@sdk/types/entities/agents/manager'; +import { noop } from '@sdk/utils'; import { createStreamApiV2 } from '../../api/streams/streamsApiV2'; import { didApiUrl } from '../../config/environment'; import { latencyTimestampTracker } from '../analytics/timestamp-tracker'; @@ -30,6 +31,7 @@ import type { Track, TranscriptionSegment, } from 'livekit-client'; +import { createVideoStatsMonitor } from './stats/poll'; async function importLiveKit(): Promise<{ Room: typeof Room; @@ -93,6 +95,7 @@ export async function createLiveKitStreamingManager | null = null; // We defer Connected until video track is subscribed to align with WebRTC behavior let hasEmittedConnected = false; @@ -262,6 +265,15 @@ export async function createLiveKitStreamingManager track.getRTCStatsReport(), + () => isConnected, + noop, + (state, _report) => { + log(`Video state change: ${state}`); + } + ); + videoStatsMonitor.start(); } } @@ -269,7 +281,11 @@ export async function createLiveKitStreamingManager Promise, getIsConnected: () => boolean, onConnected: () => void, onVideoStateChange?: (state: StreamingState, statsReport?: VideoRTCStatsReport) => void, onConnectivityStateChange?: (state: ConnectivityState) => void ) { + let intervalId: ReturnType | null = null; + let allStats: SlimRTCStatsReport[] = []; let previousStats: SlimRTCStatsReport; let notReceivingNumIntervals = 0; @@ -57,8 +59,12 @@ export function pollStats( const isReceivingVideoBytes = createVideoStatsAnalyzer(); - return setInterval(async () => { - const stats = await peerConnection.getStats(); + async function getAndAnalyzeVideoStats() { + const stats = await getStats(); + if (!stats) { + return; + } + const { isReceiving, avgJitterDelayInInterval, freezeCount } = isReceivingVideoBytes(stats); const slimStats = formatStats(stats); @@ -105,5 +111,22 @@ export function pollStats( isStreaming = false; } } - }, interval); + } + + return { + start: () => { + if (intervalId) return; + + intervalId = setInterval(getAndAnalyzeVideoStats, interval); + }, + stop: () => { + if (!intervalId) return; + + clearInterval(intervalId); + intervalId = null; + }, + getReport: (): VideoRTCStatsReport => { + return createVideoStatsReport(allStats, interval, previousStats); + }, + }; } diff --git a/src/services/streaming-manager/webrtc-core.test.ts b/src/services/streaming-manager/webrtc-core.test.ts index 5e5044ba..b0d69978 100644 --- a/src/services/streaming-manager/webrtc-core.test.ts +++ b/src/services/streaming-manager/webrtc-core.test.ts @@ -5,16 +5,21 @@ import { StreamApiFactory, StreamingAgentFactory, StreamingManagerOptionsFactory } from '../../test-utils/factories'; import { ConnectionState, CreateStreamOptions, StreamType, StreamingManagerOptions } from '../../types/index'; -import { pollStats } from './stats/poll'; +import { createVideoStatsMonitor } from './stats/poll'; import { createWebRTCStreamingManager as createStreamingManager } from './webrtc-manager'; // Mock createStreamApi const mockApi = StreamApiFactory.build(); jest.mock('../../api/streams', () => ({ createStreamApi: jest.fn(() => mockApi) })); -// Mock pollStats +// Mock createVideoStatsMonitor +const mockVideoStatsMonitor = { + start: jest.fn(), + stop: jest.fn(), + getReport: jest.fn(() => ({})), +}; jest.mock('./stats/poll', () => ({ - pollStats: jest.fn(() => 123), // mock interval id + createVideoStatsMonitor: jest.fn(() => mockVideoStatsMonitor), })); // Mock other dependencies as needed @@ -243,7 +248,7 @@ describe('Streaming Manager Core', () => { }); await createStreamingManager(agentId, agentStreamOptions, options); - expect(pollStats).toHaveBeenCalledWith( + expect(createVideoStatsMonitor).toHaveBeenCalledWith( expect.anything(), expect.anything(), expect.anything(), @@ -358,7 +363,7 @@ describe('Streaming Manager Core', () => { it('should handle pollStats initialization', async () => { const manager = await createStreamingManager(agentId, agentStreamOptions, options); - expect(pollStats).toHaveBeenCalledWith( + expect(createVideoStatsMonitor).toHaveBeenCalledWith( expect.anything(), expect.anything(), expect.anything(), @@ -368,5 +373,31 @@ describe('Streaming Manager Core', () => { expect(manager.streamId).toBe('streamId'); }); + + it('should call videoStatsMonitor.start when streaming manager is created', async () => { + await createStreamingManager(agentId, agentStreamOptions, options); + + expect(mockVideoStatsMonitor.start).toHaveBeenCalledTimes(1); + }); + + it('should call videoStatsMonitor.stop when disconnect is called', async () => { + const manager = await createStreamingManager(agentId, agentStreamOptions, options); + const mockPC = (window.RTCPeerConnection as any).mock.results[0].value; + mockPC.iceConnectionState = 'connected'; + + await manager.disconnect(); + + expect(mockVideoStatsMonitor.stop).toHaveBeenCalledTimes(1); + }); + + it('should call videoStatsMonitor.stop even when connection is in new state', async () => { + const manager = await createStreamingManager(agentId, agentStreamOptions, options); + const mockPC = (window.RTCPeerConnection as any).mock.results[0].value; + mockPC.iceConnectionState = 'new'; + + await manager.disconnect(); + + expect(mockVideoStatsMonitor.stop).toHaveBeenCalledTimes(1); + }); }); }); diff --git a/src/services/streaming-manager/webrtc-manager.ts b/src/services/streaming-manager/webrtc-manager.ts index 38a11f26..d5bbe35c 100644 --- a/src/services/streaming-manager/webrtc-manager.ts +++ b/src/services/streaming-manager/webrtc-manager.ts @@ -11,7 +11,7 @@ import { StreamType, } from '@sdk/types'; import { createStreamingLogger, StreamingManager } from './common'; -import { pollStats } from './stats/poll'; +import { createVideoStatsMonitor } from './stats/poll'; import { VideoRTCStatsReport } from './stats/report'; const actualRTCPC = ( @@ -198,8 +198,8 @@ export async function createWebRTCStreamingManager peerConnection.getStats(), getIsConnected, onConnected, (state, report) => @@ -214,6 +214,7 @@ export async function createWebRTCStreamingManager callbacks.onConnectivityStateChange?.(state) ); + videoStatsMonitor.start(); peerConnection.onicecandidate = (event: RTCPeerConnectionIceEvent) => { log('peerConnection.onicecandidate', event); @@ -343,7 +344,7 @@ export async function createWebRTCStreamingManager ({ createStreamApi: jest.fn() })); -jest.mock('./stats/poll', () => ({ pollStats: jest.fn() })); +jest.mock('./stats/poll', () => ({ + createVideoStatsMonitor: jest.fn(() => ({ + start: jest.fn(), + stop: jest.fn(), + getReport: jest.fn(() => ({})), + })), +})); jest.mock('../../config/environment', () => ({ didApiUrl: 'http://test-api.com' })); describe('Streaming Manager Utilities', () => { diff --git a/src/utils/index.ts b/src/utils/index.ts index db3cf5e8..52aa91fc 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,4 @@ +export const noop = (..._args: unknown[]) => {}; export const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); export const getRandom = (length: number = 16) => { const arr = new Uint8Array(length);