Skip to content

Commit a468720

Browse files
author
Aidan Zimmermann
committed
STREAM-1153: use multiple streams instead of multiple tracks
1 parent cdc72ad commit a468720

3 files changed

Lines changed: 116 additions & 102 deletions

File tree

src/sessions/live-monitoring-session-handler.ts

Lines changed: 20 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -71,46 +71,34 @@ export class LiveMonitoringSessionHandler extends BaseSessionHandler {
7171
}
7272

7373
async acceptSessionForObserver(session: LiveScreenMonitoringSession, params: IAcceptSessionRequest) {
74-
const videoElement = params.videoElement || this.sdk._config.defaults.videoElement;
74+
const videoElements = params.videoElements || (params.videoElement ? [params.videoElement] : [this.sdk._config.defaults.videoElement].filter(Boolean));
7575
const sessionInfo = { conversationId: session.conversationId, sessionId: session.id };
7676

77-
if (!videoElement) {
77+
if (!videoElements.length) {
7878
throw createAndEmitSdkError.call(this.sdk, SdkErrorTypes.invalid_options,
79-
'acceptSession for live monitoring observer requires a videoElement to be provided or in the default config',
79+
'acceptSession for live monitoring observer requires videoElements array or videoElement to be provided or in the default config',
8080
sessionInfo);
8181
}
8282

83-
const attachParams = { videoElement };
84-
85-
const handleIncomingTracks = (session: IExtendedMediaSession, tracks: MediaStreamTrack | MediaStreamTrack[]) => {
86-
if (!Array.isArray(tracks)) tracks = [tracks];
87-
88-
for (const track of tracks) {
89-
this.log('info', 'Incoming track from live monitoring session', {
90-
track,
91-
conversationId: session.conversationId,
92-
sessionId: session.id,
93-
sessionType: session.sessionType
94-
});
95-
96-
this.attachIncomingTrackToElement(track, attachParams);
97-
}
83+
// Use mediaStreams if provided
84+
if (params.mediaStreams && params.mediaStreams.length > 0) {
85+
this.log('info', `Attaching ${params.mediaStreams.length} media streams to ${videoElements.length} video elements`, sessionInfo);
86+
87+
params.mediaStreams.forEach((mediaStreamItem, index) => {
88+
if (index < videoElements.length) {
89+
const videoElement = videoElements[index];
90+
videoElement.muted = true;
91+
videoElement.autoplay = true;
92+
videoElement.srcObject = mediaStreamItem.stream;
93+
this.log('info', `Attached media stream to video element at index ${index}`, {
94+
streamId: mediaStreamItem.stream.id,
95+
metadata: mediaStreamItem.metadata,
96+
...sessionInfo
97+
});
98+
}
99+
});
98100

99101
session.emit('incomingMedia');
100-
};
101-
102-
// Get existing tracks
103-
const tracks = session.pc.getReceivers()
104-
.filter(receiver => receiver.track)
105-
.map(receiver => receiver.track);
106-
107-
if (tracks.length) {
108-
handleIncomingTracks(session, tracks);
109-
} else {
110-
// Listen for tracks that arrive later
111-
session.on('peerTrackAdded', (track: MediaStreamTrack) => {
112-
handleIncomingTracks(session, track);
113-
});
114102
}
115103
}
116104

@@ -129,29 +117,6 @@ export class LiveMonitoringSessionHandler extends BaseSessionHandler {
129117
this.log('warn', 'Cannot update outgoing media for live monitoring sessions', { sessionId: session.id, sessionType: session.sessionType });
130118
throw createAndEmitSdkError.call(this.sdk, SdkErrorTypes.not_supported, 'Cannot update outgoing media for live monitoring sessions');
131119
}
132-
133-
/**
134-
* Attach incoming track to HTML element
135-
*/
136-
attachIncomingTrackToElement(
137-
track: MediaStreamTrack,
138-
{ videoElement }: { videoElement: HTMLVideoElement }
139-
): HTMLAudioElement | HTMLVideoElement {
140-
const element = videoElement;
141-
142-
if (track.kind === 'video') {
143-
if (element) {
144-
element.muted = true;
145-
}
146-
}
147-
148-
if (element) {
149-
element.autoplay = true;
150-
element.srcObject = createNewStreamWithTrack(track);
151-
}
152-
153-
return element;
154-
}
155120
}
156121

157122
export default LiveMonitoringSessionHandler;

src/types/interfaces.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,11 @@ export interface IAcceptSessionRequest extends ISdkMediaDeviceIds {
667667
*/
668668
mediaStream?: MediaStream;
669669

670+
/**
671+
* array of media streams with associated metadata
672+
*/
673+
mediaStreams?: { stream: MediaStream; metadata: ScreenRecordingMetadata }[];
674+
670675
/**
671676
* metadata about screens and tracks. This is required for screen recording sessions
672677
*/
@@ -678,6 +683,9 @@ export interface IAcceptSessionRequest extends ISdkMediaDeviceIds {
678683
/** video element to attach incoming video to. default is sdk `defaults.videoElement` */
679684
videoElement?: HTMLVideoElement;
680685

686+
/** array of video elements for live monitoring observers to attach multiple video streams */
687+
videoElements?: HTMLVideoElement[];
688+
681689
/** Flag set to true when the participant is a monitoring observer. default is `false` */
682690
liveMonitoringObserver?: boolean
683691
}

test/unit/sessions/live-monitoring-session-handler.test.ts

Lines changed: 88 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ describe('handlePropose', () => {
6262

6363
mockSdk._config.autoAcceptPendingLiveScreenMonitoringRequests = false;
6464
const pendingSession = { fromUserId: 'user123' } as any;
65-
65+
6666
await handler.handlePropose(pendingSession);
6767

6868
expect(handler._liveMonitoringObserver).toBe(false);
@@ -76,7 +76,7 @@ describe('handlePropose', () => {
7676

7777
mockSdk._config.autoAcceptPendingLiveScreenMonitoringRequests = false;
7878
const pendingSession = { fromUserId: 'different-user' } as any;
79-
79+
8080
await handler.handlePropose(pendingSession);
8181

8282
expect(handler._liveMonitoringObserver).toBe(false);
@@ -157,55 +157,113 @@ describe('acceptSessionForTarget', () => {
157157
});
158158

159159
describe('acceptSessionForObserver', () => {
160-
let parentHandlerSpy: jest.SpyInstance<Promise<any>>;
161-
let addMediaToSessionSpy: jest.SpyInstance<Promise<void>>;
162-
let attachIncomingTrackToElementSpy: jest.SpyInstance<HTMLAudioElement>;
163-
let startMediaSpy: jest.SpyInstance<Promise<MediaStream>>;
164-
let initialMutesSpy: jest.SpyInstance<Promise<any>>; /* keep this spy */
165160
let session: LiveScreenMonitoringSession;
166-
let media: MediaStream;
167161

168162
beforeEach(() => {
169-
media = new MockStream() as any;
170-
parentHandlerSpy = jest.spyOn(BaseSessionHandler.prototype, 'acceptSession').mockResolvedValue(null);
171-
attachIncomingTrackToElementSpy = jest.spyOn(handler, 'attachIncomingTrackToElement').mockReturnValue({} as HTMLMediaElement);
172-
startMediaSpy = jest.spyOn(mockSdk.media, 'startMedia').mockResolvedValue(media);
173163
session = new MockSession() as any;
174164
});
175165

176166
it('should throw if no video element is provided', async () => {
177-
await expect(handler.acceptSession(session, { conversationId: session.conversationId, liveMonitoringObserver: true, audioElement: document.createElement('audio') })).rejects.toThrowError(/requires a videoElement/);
167+
await expect(handler.acceptSession(session, { conversationId: session.conversationId, liveMonitoringObserver: true, audioElement: document.createElement('audio') })).rejects.toThrowError(/requires videoElements array or videoElement/);
168+
});
169+
170+
it('should attach mediaStreams to video elements when provided', async () => {
171+
const video1 = document.createElement('video');
172+
const video2 = document.createElement('video');
173+
const videoElements = [video1, video2];
174+
175+
const stream1 = new MockStream({ video: true }) as any;
176+
const stream2 = new MockStream({ video: true }) as any;
177+
const metadata1 = { screenId: 'screen1', trackId: 'track1', originX: 0, originY: 0, resolutionX: 1920, resolutionY: 1080, primary: true };
178+
const metadata2 = { screenId: 'screen2', trackId: 'track2', originX: 1920, originY: 0, resolutionX: 1920, resolutionY: 1080, primary: false };
179+
180+
const mediaStreams = [
181+
{ stream: stream1, metadata: metadata1 },
182+
{ stream: stream2, metadata: metadata2 }
183+
];
184+
185+
const emitSpy = jest.spyOn(session, 'emit');
186+
187+
await handler.acceptSession(session, {
188+
conversationId: session.conversationId,
189+
liveMonitoringObserver: true,
190+
videoElements,
191+
mediaStreams
192+
});
193+
194+
expect(video1.srcObject).toBe(stream1);
195+
expect(video1.muted).toBe(true);
196+
expect(video1.autoplay).toBe(true);
197+
198+
expect(video2.srcObject).toBe(stream2);
199+
expect(video2.muted).toBe(true);
200+
expect(video2.autoplay).toBe(true);
201+
202+
expect(emitSpy).toHaveBeenCalledWith('incomingMedia');
178203
});
179204

180-
it('should use default elements', async () => {
181-
const video = mockSdk._config.defaults!.videoElement = document.createElement('video');
205+
it('should only attach streams up to the number of available video elements', async () => {
206+
const video1 = document.createElement('video');
207+
const videoElements = [video1]; // Only one video element
182208

183-
const incomingTrack = {} as any;
184-
jest.spyOn(session.pc, 'getReceivers').mockReturnValue([{ track: incomingTrack }] as any);
209+
const stream1 = new MockStream({ video: true }) as any;
210+
const stream2 = new MockStream({ video: true }) as any;
211+
const metadata = { screenId: 'screen1', trackId: 'track1', originX: 0, originY: 0, resolutionX: 1920, resolutionY: 1080, primary: true };
185212

186-
attachIncomingTrackToElementSpy.mockReturnValue(video);
213+
const mediaStreams = [
214+
{ stream: stream1, metadata },
215+
{ stream: stream2, metadata } // This won't be attached
216+
];
187217

188-
await handler.acceptSession(session, { conversationId: session.conversationId, liveMonitoringObserver: true });
218+
await handler.acceptSession(session, {
219+
conversationId: session.conversationId,
220+
liveMonitoringObserver: true,
221+
videoElements,
222+
mediaStreams
223+
});
189224

190-
expect(parentHandlerSpy).toHaveBeenCalled();
191-
expect(attachIncomingTrackToElementSpy).toHaveBeenCalledWith(incomingTrack, { videoElement: video });
225+
expect(video1.srcObject).toBe(stream1);
226+
// stream2 should not be attached anywhere
192227
});
193228

229+
it('should use videoElement field when no videoElements provided ', async () => {
230+
const videoElement = document.createElement('video');
231+
232+
const stream1 = new MockStream({ video: true }) as any;
233+
const stream2 = new MockStream({ video: true }) as any;
234+
const metadata = { screenId: 'screen1', trackId: 'track1', originX: 0, originY: 0, resolutionX: 1920, resolutionY: 1080, primary: true };
194235

195-
it('should attach tracks later if not available', async () => {
196-
const video = document.createElement('video');
236+
const mediaStreams = [
237+
{ stream: stream1, metadata },
238+
{ stream: stream2, metadata } // This won't be attached
239+
];
197240

198-
attachIncomingTrackToElementSpy.mockReturnValue(video);
241+
await handler.acceptSession(session, {
242+
conversationId: session.conversationId,
243+
liveMonitoringObserver: true,
244+
videoElement,
245+
mediaStreams
246+
});
199247

200-
await handler.acceptSession(session, { conversationId: session.conversationId, liveMonitoringObserver: true, videoElement: video });
201-
expect(attachIncomingTrackToElementSpy).not.toHaveBeenCalled();
202-
expect(parentHandlerSpy).toHaveBeenCalled();
248+
expect(videoElement.srcObject).toBe(stream1);
249+
// stream2 should not be attached anywhere
250+
});
203251

204-
const incomingTrack = {} as any;
252+
it('should use default video element when no videoElements or videoElement provided', async () => {
253+
const defaultVideo = document.createElement('video');
254+
mockSdk._config.defaults!.videoElement = defaultVideo;
205255

206-
session.emit('peerTrackAdded', incomingTrack);
256+
const stream = new MockStream({ video: true }) as any;
257+
const metadata = { screenId: 'screen1', trackId: 'track1', originX: 0, originY: 0, resolutionX: 1920, resolutionY: 1080, primary: true };
258+
const mediaStreams = [{ stream, metadata }];
207259

208-
expect(attachIncomingTrackToElementSpy).toHaveBeenCalledWith(incomingTrack, { videoElement: video });
260+
await handler.acceptSession(session, {
261+
conversationId: session.conversationId,
262+
liveMonitoringObserver: true,
263+
mediaStreams
264+
});
265+
266+
expect(defaultVideo.srcObject).toBe(stream);
209267
});
210268
});
211269

@@ -243,20 +301,3 @@ describe('updateOutgoingMedia', () => {
243301
);
244302
});
245303
});
246-
247-
describe('attachIncomingTrackToElement', () => {
248-
it('should attach to video element', () => {
249-
const video = document.createElement('video');
250-
251-
const track = new MockTrack();
252-
track.kind = 'video';
253-
const fakeStream = {};
254-
jest.spyOn(mediaUtils, 'createNewStreamWithTrack').mockReturnValue(fakeStream as any);
255-
256-
handler.attachIncomingTrackToElement(track as any, { videoElement: video });
257-
258-
expect(video.srcObject).toBe(fakeStream);
259-
expect(video.autoplay).toBeTruthy();
260-
expect(video.muted).toBeTruthy();
261-
});
262-
});

0 commit comments

Comments
 (0)