From 346cef9317c105f094f36eac4588de5ed84e0496 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 29 Jan 2026 10:06:50 +0000 Subject: [PATCH 1/2] chore: sync version 1.1.47 from prod [skip ci] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 396df878..79781c29 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@d-id/client-sdk", "private": false, - "version": "1.1.46", + "version": "1.1.47", "type": "module", "description": "d-id client sdk", "repository": { From 3c0f8ecdeffb630a2f440c66e418a33365d2e5ce Mon Sep 17 00:00:00 2001 From: Ofek Simhi <158498125+osimhi213@users.noreply.github.com> Date: Fri, 30 Jan 2026 11:43:53 +0200 Subject: [PATCH 2/2] Bugfix/livekit session starts with poor resolution (#312) * Bugfix/session starting with low bitrate * fix * CR --- .../streaming-manager/livekit-manager.test.ts | 105 +++++++++++------- .../streaming-manager/livekit-manager.ts | 35 +++++- 2 files changed, 91 insertions(+), 49 deletions(-) diff --git a/src/services/streaming-manager/livekit-manager.test.ts b/src/services/streaming-manager/livekit-manager.test.ts index 089cb05d..dfe3ce81 100644 --- a/src/services/streaming-manager/livekit-manager.test.ts +++ b/src/services/streaming-manager/livekit-manager.test.ts @@ -75,14 +75,28 @@ jest.mock('../../api/streams/streamsApiV2', () => ({ jest.mock('../../config/environment', () => ({ didApiUrl: 'http://test-api.com' })); -// Mock createVideoStatsMonitor +// Mock VideoStatsMonitor const mockVideoStatsMonitor = { start: jest.fn(), stop: jest.fn(), getReport: jest.fn(() => ({})), + _onVideoStateChange: null as any | null, + invokeStateChange(state: StreamingState, report?: unknown) { + this._onVideoStateChange?.(state, report); + }, }; + jest.mock('./stats/poll', () => ({ - createVideoStatsMonitor: jest.fn(() => mockVideoStatsMonitor), + createVideoStatsMonitor: jest.fn( + (_getStats: unknown, _getIsConnected: unknown, _onConnected: unknown, onVideoStateChange: any) => { + mockVideoStatsMonitor._onVideoStateChange = onVideoStateChange; + return { + start: mockVideoStatsMonitor.start, + stop: mockVideoStatsMonitor.stop, + getReport: mockVideoStatsMonitor.getReport, + }; + } + ), })); const mockLatencyTimestampTrackerUpdate = jest.fn(); @@ -99,7 +113,6 @@ const TEST_AGENT_ID = 'agent123'; const TEST_TRACK_SID = 'track-sid-123'; const TEST_AUDIO_TRACK_ID = 'audio-track-1'; const TEST_AUDIO_TRACK_ID_2 = 'audio-track-2'; -const TEST_SESSION_ID = 'session-123'; const ASYNC_WAIT_TIME = 10; // Helper functions to create mock objects @@ -539,75 +552,81 @@ describe('LiveKit Streaming Manager - Microphone Stream', () => { expect(mockVideoStatsMonitor.start).not.toHaveBeenCalled(); }); - it('should stop video stats monitor when video track is unsubscribed', async () => { + it('should call getReport and onVideoStateChange with Stop and report when video track is unsubscribed', async () => { + const onVideoStateChange = jest.fn(); + const report = { duration: 1000 }; + options.callbacks.onVideoStateChange = onVideoStateChange; + mockVideoStatsMonitor.getReport.mockReturnValue(report); + await createLiveKitStreamingManager(agentId, sessionOptions, options); await simulateConnection(); - const trackSubscribedHandler = getTrackSubscribedHandler(); - const trackUnsubscribedHandler = getTrackUnsubscribedHandler(); const mockVideoTrack = createMockVideoTrack(); - const mockParticipant = createMockRemoteParticipant(); + getTrackSubscribedHandler()(mockVideoTrack, {}, createMockRemoteParticipant()); + mockVideoStatsMonitor.invokeStateChange(StreamingState.Start); + onVideoStateChange.mockClear(); + mockVideoStatsMonitor.getReport.mockClear(); - trackSubscribedHandler(mockVideoTrack, {}, mockParticipant); - expect(mockVideoStatsMonitor.start).toHaveBeenCalledTimes(1); - trackUnsubscribedHandler(mockVideoTrack, {}, mockParticipant); + getTrackUnsubscribedHandler()(mockVideoTrack, {}, createMockRemoteParticipant()); + expect(mockVideoStatsMonitor.getReport).toHaveBeenCalledTimes(1); expect(mockVideoStatsMonitor.stop).toHaveBeenCalledTimes(1); + expect(onVideoStateChange).toHaveBeenCalledTimes(1); + expect(onVideoStateChange).toHaveBeenCalledWith(StreamingState.Stop, report); }); - it('should get report from video stats monitor when video track is unsubscribed', async () => { + it('should call onVideoStateChange with Start when videoStatsMonitor callback is invoked with Start', async () => { + const onVideoStateChange = jest.fn(); + options.callbacks.onVideoStateChange = onVideoStateChange; + await createLiveKitStreamingManager(agentId, sessionOptions, options); await simulateConnection(); - const mockReport = { duration: 1000 }; - mockVideoStatsMonitor.getReport.mockReturnValue(mockReport); + getTrackSubscribedHandler()(createMockVideoTrack(), {}, createMockRemoteParticipant()); - const trackSubscribedHandler = getTrackSubscribedHandler(); - const trackUnsubscribedHandler = getTrackUnsubscribedHandler(); - const mockVideoTrack = createMockVideoTrack(); - const mockParticipant = createMockRemoteParticipant(); + onVideoStateChange.mockClear(); + mockVideoStatsMonitor.invokeStateChange(StreamingState.Start); - trackSubscribedHandler(mockVideoTrack, {}, mockParticipant); - trackUnsubscribedHandler(mockVideoTrack, {}, mockParticipant); - - expect(mockVideoStatsMonitor.getReport).toHaveBeenCalledTimes(1); + expect(onVideoStateChange).toHaveBeenCalledTimes(1); + expect(onVideoStateChange).toHaveBeenCalledWith(StreamingState.Start); }); - it('should call onVideoStateChange with Start when video track is subscribed', async () => { - const mockOnVideoStateChange = jest.fn(); - options.callbacks.onVideoStateChange = mockOnVideoStateChange; + it('should call onVideoStateChange with Stop and report when videoStatsMonitor callback is invoked with Stop', async () => { + const onVideoStateChange = jest.fn(); + const report = { duration: 1000 }; + options.callbacks.onVideoStateChange = onVideoStateChange; await createLiveKitStreamingManager(agentId, sessionOptions, options); await simulateConnection(); + getTrackSubscribedHandler()(createMockVideoTrack(), {}, createMockRemoteParticipant()); + mockVideoStatsMonitor.invokeStateChange(StreamingState.Start); - const trackSubscribedHandler = getTrackSubscribedHandler(); - const mockVideoTrack = createMockVideoTrack(); - const mockParticipant = createMockRemoteParticipant(); - - trackSubscribedHandler(mockVideoTrack, {}, mockParticipant); + onVideoStateChange.mockClear(); + mockVideoStatsMonitor.invokeStateChange(StreamingState.Stop, report); - expect(mockOnVideoStateChange).toHaveBeenCalledWith(StreamingState.Start); + expect(mockVideoStatsMonitor.getReport).not.toHaveBeenCalled(); + expect(onVideoStateChange).toHaveBeenCalledTimes(1); + expect(onVideoStateChange).toHaveBeenCalledWith(StreamingState.Stop, report); }); - 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); + it('should not call onVideoStateChange(Stop) twice when monitor Stop and track unsubscribed both occur', async () => { + const onVideoStateChange = jest.fn(); + const report = { duration: 1000 }; + options.callbacks.onVideoStateChange = onVideoStateChange; + mockVideoStatsMonitor.getReport.mockReturnValue(report); await createLiveKitStreamingManager(agentId, sessionOptions, options); await simulateConnection(); - const trackSubscribedHandler = getTrackSubscribedHandler(); - const trackUnsubscribedHandler = getTrackUnsubscribedHandler(); const mockVideoTrack = createMockVideoTrack(); - const mockParticipant = createMockRemoteParticipant(); - - trackSubscribedHandler(mockVideoTrack, {}, mockParticipant); - mockOnVideoStateChange.mockClear(); + getTrackSubscribedHandler()(mockVideoTrack, {}, createMockRemoteParticipant()); + mockVideoStatsMonitor.invokeStateChange(StreamingState.Start); + onVideoStateChange.mockClear(); - trackUnsubscribedHandler(mockVideoTrack, {}, mockParticipant); + mockVideoStatsMonitor.invokeStateChange(StreamingState.Stop, report); + getTrackUnsubscribedHandler()(mockVideoTrack, {}, createMockRemoteParticipant()); - expect(mockOnVideoStateChange).toHaveBeenCalledWith(StreamingState.Stop, mockReport); + expect(onVideoStateChange).toHaveBeenCalledTimes(1); + expect(onVideoStateChange).toHaveBeenCalledWith(StreamingState.Stop, report); }); }); diff --git a/src/services/streaming-manager/livekit-manager.ts b/src/services/streaming-manager/livekit-manager.ts index b0f178da..13ddd1ec 100644 --- a/src/services/streaming-manager/livekit-manager.ts +++ b/src/services/streaming-manager/livekit-manager.ts @@ -33,6 +33,7 @@ import type { TranscriptionSegment, } from 'livekit-client'; import { createVideoStatsMonitor } from './stats/poll'; +import { VideoRTCStatsReport } from './stats/report'; async function importLiveKit(): Promise<{ Room: typeof Room; @@ -99,6 +100,7 @@ export async function createLiveKitStreamingManager | null = null; + let videoStreamingState: StreamingState | null = null; // We defer Connected until video track is subscribed to align with WebRTC behavior let hasEmittedConnected = false; @@ -227,6 +229,26 @@ export async function createLiveKitStreamingManager track.getRTCStatsReport(), () => isConnected, noop, - (state, _report) => { + (state, report) => { log(`Video state change: ${state}`); + if (state === StreamingState.Start) { + handleVideoStarted(); + } else if (state === StreamingState.Stop) { + handleVideoStopped(report); + } } ); videoStatsMonitor.start(); @@ -278,11 +303,9 @@ export async function createLiveKitStreamingManager