Skip to content

Commit 7b2ce97

Browse files
authored
Merge pull request #953 from MyPureCloud/MR-825
STREAM-825: add support for multiple video tracks
2 parents 7fa05d1 + 3937ac0 commit 7b2ce97

6 files changed

Lines changed: 97 additions & 82 deletions

File tree

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ test/test-pages/*/index.html
1818
## vscode specific files
1919
/.vscode
2020

21-
## webstorm specific files
21+
## jetbrains specific files
2222
/.idea
23+
genesys-cloud-webrtc-sdk.iml
2324

2425
# tap specific files
2526
/.nyc_output
@@ -28,3 +29,5 @@ test/test-pages/*/index.html
2829
/.npmrc
2930

3031
/test-results
32+
33+
.DS_STORE

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66
# [Unreleased](https://github.com/MyPureCloud/genesys-cloud-webrtc-sdk/compare/v11.3.4...HEAD)
77
### Added
88
* [STREAM-992](https://inindca.atlassian.net/browse/STREAM-992) - Added pull request template checklist.
9+
* [STREAM-825](https://inindca.atlassian.net/browse/STREAM-825) - Add ability to send multiple tracks when screensharing
910
### Fixed
1011
* [STREAM-990](https://inindca.atlassian.net/browse/STREAM-990) - Demo app: Store a copy of pendingSessions so freezing them doesn't affect the SDK's usage of those objects
1112

src/sessions/base-session-handler.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,8 @@ export default abstract class BaseSessionHandler {
477477
}
478478

479479
this.log('debug', 'Adding track to session', { track: t, conversationId: session.conversationId, sessionId: session.id, sessionType: session.sessionType });
480-
promises.push(session.peerConnection.addTrack(t));
480+
// Use addTrack with the stream to ensure proper track handling for multiple tracks of the same kind
481+
promises.push(session.peerConnection.addTrack(t, stream));
481482
});
482483
} else {
483484
const errMsg = 'Track based actions are required for this session but the client is not capable';

src/sessions/video-session-handler.ts

Lines changed: 23 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,13 @@ export class VideoSessionHandler extends BaseSessionHandler {
550550
session.pc.getSenders().forEach(sender => sender.track && sender.track.stop());
551551
}
552552

553+
getCameraTrackForSession (session: IExtendedMediaSession): MediaStreamTrack {
554+
return session._outboundStream.getVideoTracks().find(track => {
555+
// Screen share tracks are in the _screenShareStream, so any video track in _outboundStream should be camera
556+
return !session._screenShareStream || !session._screenShareStream.getVideoTracks().find(screenTrack => screenTrack.id === track.id);
557+
})
558+
}
559+
553560
async setVideoMute (session: IExtendedMediaSession, params: ISessionMuteRequest, skipServerUpdate?: boolean): Promise<void> {
554561
const replayMuteRequest = !!session.videoMuted === !!params.mute;
555562

@@ -562,25 +569,22 @@ export class VideoSessionHandler extends BaseSessionHandler {
562569

563570
const userId = this.sdk._personDetails.id;
564571

565-
// if we are going to mute, we need to remove/end the existing camera track
572+
// if we are going to mute, we need to remove the existing camera video track but not any screen share video tracks
566573
if (params.mute) {
567-
// get the first video track
568-
const track = session._outboundStream.getVideoTracks().find(t => t);
574+
const cameraTrack = this.getCameraTrackForSession(session);
569575

570-
if (!track) {
576+
if (!cameraTrack) {
571577
this.log('warn', 'Unable to find outbound camera track', { sessionId: session.id, conversationId: session.conversationId, sessionType: session.sessionType });
572578
} else {
573-
574579
const sender = this.getSendersByTrackType(session, 'video')
575-
.find((sender) => sender.track && sender.track.id === track.id);
580+
.find((sender) => sender.track && sender.track.id === cameraTrack.id);
576581

577582
if (sender) {
578583
await this.removeMediaFromSession(session, sender);
579584
}
580585

581-
track.stop();
582-
session._outboundStream.removeTrack(track);
583-
586+
cameraTrack.stop();
587+
session._outboundStream.removeTrack(cameraTrack);
584588
}
585589

586590
if (!skipServerUpdate) {
@@ -589,9 +593,11 @@ export class VideoSessionHandler extends BaseSessionHandler {
589593

590594
// if we are unmuting, we need to get a new camera track and add that to the session
591595
} else {
592-
// Make sure we don't have any tracks before we decide to spin up another.
593-
if (session._outboundStream.getVideoTracks().length > 0) {
594-
this.log('debug', 'Cannot unmute, a video track already exists', { conversationId: session.conversationId, sessionId: session.id, sessionType: session.sessionType });
596+
// Check if we already have a camera track (exclude screen share tracks)
597+
const existingCameraTrack = this.getCameraTrackForSession(session);
598+
599+
if (existingCameraTrack) {
600+
this.log('debug', 'Cannot unmute, a camera video track already exists', { conversationId: session.conversationId, sessionId: session.id, sessionType: session.sessionType });
595601
return;
596602
}
597603

@@ -714,11 +720,8 @@ export class VideoSessionHandler extends BaseSessionHandler {
714720

715721
this.log('info', 'Screen media created', { sessionId: session.id, conversationId: session.conversationId, sessionType: session.sessionType });
716722

717-
await this.addReplaceTrackToSession(session, stream.getVideoTracks()[0]);
718-
719-
if (!session.videoMuted) {
720-
await this.setVideoMute(session, { conversationId: session.conversationId, mute: true }, true);
721-
}
723+
// Add screen share track without replacing existing video track
724+
await this.addMediaToSession(session, stream);
722725

723726
stream.getTracks().forEach((track: MediaStreamTrack) => {
724727
track.addEventListener('ended', this.stopScreenShare.bind(this, session));
@@ -747,21 +750,9 @@ export class VideoSessionHandler extends BaseSessionHandler {
747750
const track = session._screenShareStream.getVideoTracks()[0];
748751
const sender = session.pc.getSenders().find(sender => sender.track && sender.track.id === track.id);
749752

750-
if (session._resurrectVideoOnScreenShareEnd) {
751-
this.log('info', 'Restarting video track', { conversationId: session.conversationId, sessionId: session.id, sessionType: session.sessionType });
752-
try {
753-
await this.setVideoMute(session, { conversationId: session.conversationId, mute: false }, true);
754-
} catch (err) {
755-
/* This is to ensure that if something goes wrong while
756-
* fetching the preferred device's media, we still stop screensharing
757-
* but return the user to a muted state
758-
*/
759-
await this.setVideoMute(session, { conversationId: session.conversationId, mute: true }, false);
760-
track.stop();
761-
this.sessionManager.webrtcSessions.notifyScreenShareStop(session);
762-
}
763-
} else {
764-
await sender.replaceTrack(null);
753+
if (sender) {
754+
// Remove the screen share track without affecting other video tracks
755+
await this.removeMediaFromSession(session, sender);
765756
}
766757

767758
track.stop();

test/unit/sessions/softphone-session-handler.test.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import nock from 'nock';
2-
31
import {
42
SimpleMockSdk,
53
MockSession,
@@ -52,7 +50,6 @@ beforeAll(() => {
5250

5351
beforeEach(() => {
5452
jest.clearAllMocks();
55-
nock.cleanAll();
5653
mockSdk = (new SimpleMockSdk() as any);
5754
(mockSdk as any).isGuest = true;
5855
mockSdk._config.autoConnectSessions = true;

test/unit/sessions/video-session-handler.test.ts

Lines changed: 67 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import nock from 'nock';
21
import { v4 as uuidv4 } from 'uuid';
32

43
import { SimpleMockSdk, MockSession, MockStream, MockTrack, random } from '../../test-utils';
@@ -25,7 +24,6 @@ let userId: string;
2524

2625
beforeEach(() => {
2726
jest.clearAllMocks();
28-
nock.cleanAll();
2927
mockSdk = (new SimpleMockSdk() as any);
3028
(mockSdk as any).isGuest = true;
3129
mockSdk._config.autoConnectSessions = true;
@@ -1006,7 +1004,7 @@ describe('setInitialMuteStates', () => {
10061004
beforeEach(() => {
10071005
session = new MockSession() as any;
10081006
audioSender = { track: { kind: 'audio', enabled: true } };
1009-
videoSender = { track: { kind: 'video', enabled: true } };
1007+
videoSender = { track: { kind: 'video', enabled: true, id: 'camera-track-1' } };
10101008
});
10111009

10121010
it('should mute video', async () => {
@@ -1219,7 +1217,7 @@ describe('setVideoMute', () => {
12191217

12201218
await handler.setVideoMute(session, { conversationId: session.conversationId, mute: false });
12211219

1222-
expect(mockSdk.logger.debug).toHaveBeenCalledWith(expect.stringContaining('Cannot unmute, a video track already exists'), expect.any(Object), undefined);
1220+
expect(mockSdk.logger.debug).toHaveBeenCalledWith(expect.stringContaining('Cannot unmute, a camera video track already exists'), expect.any(Object), undefined);
12231221
});
12241222

12251223
it('mute: should mute video track when there is no screen track and remove track from outboundStream', async () => {
@@ -1306,7 +1304,7 @@ describe('setVideoMute', () => {
13061304
const stream = new MockStream(true);
13071305
const spy = jest.spyOn(mockSdk.media, 'startMedia').mockResolvedValue(stream as any);
13081306
jest.spyOn(mockSessionManager, 'getAllActiveSessions').mockReturnValue([{ id: session.id } as IExtendedMediaSession ]);
1309-
jest.spyOn(handler, 'addMediaToSession').mockResolvedValue();
1307+
jest.spyOn(handler, 'addReplaceTrackToSession').mockResolvedValue();
13101308

13111309
session._outboundStream = {
13121310
addTrack: jest.fn(),
@@ -1327,7 +1325,7 @@ describe('setVideoMute', () => {
13271325
const stream = new MockStream(true);
13281326
const spy = jest.spyOn(mockSdk.media, 'startMedia').mockResolvedValue(stream as any);
13291327
jest.spyOn(mockSessionManager, 'getAllActiveSessions').mockReturnValue([{ id: session.id } as IExtendedMediaSession ]);
1330-
jest.spyOn(handler, 'addMediaToSession').mockResolvedValue();
1328+
jest.spyOn(handler, 'addReplaceTrackToSession').mockResolvedValue();
13311329

13321330
session._outboundStream = {
13331331
addTrack: jest.fn(),
@@ -1347,7 +1345,7 @@ describe('setVideoMute', () => {
13471345
const unmuteDeviceId = 'device-id';
13481346
const stream = new MockStream(true);
13491347
const spy = jest.spyOn(mockSdk.media, 'startMedia').mockResolvedValue(stream as any);
1350-
jest.spyOn(handler, 'addMediaToSession').mockResolvedValue();
1348+
jest.spyOn(handler, 'addReplaceTrackToSession').mockResolvedValue();
13511349

13521350
session._outboundStream = {
13531351
addTrack: jest.fn(),
@@ -1361,6 +1359,51 @@ describe('setVideoMute', () => {
13611359
expect(session.unmute).not.toHaveBeenCalled();
13621360
expect(stream.getVideoTracks()[0].stop).toHaveBeenCalled();
13631361
});
1362+
1363+
it('mute: should find camera track when screen share exists', async () => {
1364+
const screenTrack = new MockTrack('video');
1365+
screenTrack.id = 'screen-track-id';
1366+
const cameraTrack = new MockTrack('video');
1367+
cameraTrack.id = 'camera-track-id';
1368+
1369+
session._screenShareStream = {
1370+
getVideoTracks: jest.fn().mockReturnValue([screenTrack])
1371+
} as any;
1372+
1373+
session._outboundStream = {
1374+
removeTrack: jest.fn(),
1375+
getVideoTracks: jest.fn().mockReturnValue([cameraTrack])
1376+
} as any;
1377+
1378+
jest.spyOn(handler, 'getSendersByTrackType').mockReturnValue([
1379+
{ track: cameraTrack }
1380+
] as any);
1381+
1382+
await handler.setVideoMute(session, { conversationId: session.conversationId, mute: true });
1383+
1384+
expect(cameraTrack.stop).toHaveBeenCalled();
1385+
expect(session.mute).toHaveBeenCalledWith(userId, 'video');
1386+
});
1387+
1388+
it('unmute: should check for existing camera track when screen share exists', async () => {
1389+
const screenTrack = new MockTrack('video');
1390+
screenTrack.id = 'screen-track-id';
1391+
const cameraTrack = new MockTrack('video');
1392+
cameraTrack.id = 'camera-track-id';
1393+
1394+
session._screenShareStream = {
1395+
getVideoTracks: jest.fn().mockReturnValue([screenTrack])
1396+
} as any;
1397+
1398+
session._outboundStream = {
1399+
addTrack: jest.fn(),
1400+
getVideoTracks: jest.fn().mockReturnValue([cameraTrack])
1401+
} as any;
1402+
1403+
await handler.setVideoMute(session, { conversationId: session.conversationId, mute: false });
1404+
1405+
expect(mockSdk.logger.debug).toHaveBeenCalledWith(expect.stringContaining('Cannot unmute, a camera video track already exists'), expect.any(Object), undefined);
1406+
});
13641407
});
13651408

13661409
describe('setAudioMute', () => {
@@ -1483,33 +1526,29 @@ describe('getSendersByTrackType', () => {
14831526

14841527
describe('startScreenShare', () => {
14851528
let displayMediaSpy: jest.SpyInstance<Promise<MediaStream>>;
1486-
let videoMuteSpy: jest.SpyInstance<Promise<void>>;
1487-
let addReplaceTrackToSession: jest.SpyInstance<Promise<any>>;
1529+
let addMediaToSessionSpy: jest.SpyInstance<Promise<void>>;
14881530
let session: VideoMediaSession;
14891531

14901532
beforeEach(() => {
14911533
displayMediaSpy = jest.spyOn(mockSdk.media, 'startDisplayMedia').mockResolvedValue(new MockStream({ video: true }) as any);
1492-
videoMuteSpy = jest.spyOn(handler, 'setVideoMute').mockResolvedValue();
1493-
addReplaceTrackToSession = jest.spyOn(handler, 'addReplaceTrackToSession').mockResolvedValue();
1534+
addMediaToSessionSpy = jest.spyOn(handler, 'addMediaToSession').mockResolvedValue();
14941535
session = new MockSession() as any;
14951536
});
14961537

1497-
it('should start media and mute video if it is not already muted', async () => {
1538+
it('should start media and add screen share track without replacing existing video', async () => {
14981539
await handler.startScreenShare(session);
14991540

15001541
expect(displayMediaSpy).toHaveBeenCalled();
1501-
expect(videoMuteSpy).toHaveBeenCalled();
1502-
expect(addReplaceTrackToSession).toHaveBeenCalled();
1542+
expect(addMediaToSessionSpy).toHaveBeenCalled();
15031543
expect(mockSessionManager.webrtcSessions.notifyScreenShareStart).toHaveBeenCalled();
15041544
});
15051545

1506-
it('should not mute video if already muted', async () => {
1546+
it('should add screen share track regardless of video mute state', async () => {
15071547
session.videoMuted = true;
15081548
await handler.startScreenShare(session);
15091549

15101550
expect(displayMediaSpy).toHaveBeenCalled();
1511-
expect(videoMuteSpy).not.toHaveBeenCalled();
1512-
expect(addReplaceTrackToSession).toHaveBeenCalled();
1551+
expect(addMediaToSessionSpy).toHaveBeenCalled();
15131552
expect(mockSessionManager.webrtcSessions.notifyScreenShareStart).toHaveBeenCalled();
15141553
});
15151554

@@ -1534,12 +1573,10 @@ describe('startScreenShare', () => {
15341573
});
15351574

15361575
describe('stopScreenShare', () => {
1537-
let videoMuteSpy;
15381576
let removeMediaSpy;
15391577
let session;
15401578

15411579
beforeEach(() => {
1542-
videoMuteSpy = jest.spyOn(handler, 'setVideoMute').mockResolvedValue();
15431580
removeMediaSpy = jest.spyOn(handler, 'removeMediaFromSession').mockResolvedValue();
15441581
session = new MockSession();
15451582
});
@@ -1549,49 +1586,34 @@ describe('stopScreenShare', () => {
15491586

15501587
await handler.stopScreenShare(session);
15511588

1552-
expect(videoMuteSpy).not.toHaveBeenCalled();
15531589
expect(removeMediaSpy).not.toHaveBeenCalled();
15541590
expect(mockSessionManager.webrtcSessions.notifyScreenShareStop).not.toHaveBeenCalled();
15551591
});
15561592

1557-
it('should stop screen share tracks and unmute video if resurrectVideoOnScreenShareEnd', async () => {
1558-
session._resurrectVideoOnScreenShareEnd = true;
1593+
it('should remove screen share track and stop it', async () => {
15591594
session._screenShareStream = new MockStream({ video: true });
1595+
const screenTrack = session._screenShareStream._tracks[0];
15601596

1561-
await handler.stopScreenShare(session);
1562-
1563-
expect(videoMuteSpy).toHaveBeenCalled();
1564-
expect(session._screenShareStream._tracks[0].stop).toHaveBeenCalled();
1565-
expect(mockSessionManager.webrtcSessions.notifyScreenShareStop).toHaveBeenCalled();
1566-
});
1567-
1568-
it('should not unmute video if not resurrect', async () => {
1569-
session.resurrectVideoOnScreenShareEnd = false;
1570-
session._screenShareStream = new MockStream({ video: true });
1571-
1572-
const replaceSpy = jest.fn();
1573-
jest.spyOn(session.pc, 'getSenders').mockReturnValue([{ track: session._screenShareStream._tracks[0], replaceTrack: replaceSpy }]);
1597+
jest.spyOn(session.pc, 'getSenders').mockReturnValue([{ track: screenTrack }]);
15741598

15751599
await handler.stopScreenShare(session);
15761600

1577-
expect(videoMuteSpy).not.toHaveBeenCalled();
1578-
expect(session._screenShareStream._tracks[0].stop).toHaveBeenCalled();
1579-
expect(replaceSpy).toHaveBeenCalled();
1601+
expect(removeMediaSpy).toHaveBeenCalled();
1602+
expect(screenTrack.stop).toHaveBeenCalled();
15801603
expect(mockSessionManager.webrtcSessions.notifyScreenShareStop).toHaveBeenCalled();
15811604
});
15821605

1583-
it('should toggle off of screen share AND keep video unmuted if something goes wrong when fetching device media', async () => {
1584-
videoMuteSpy = jest.spyOn(handler, 'setVideoMute').mockRejectedValueOnce({ message: 'Could not start video source' }).mockResolvedValueOnce();
1585-
session._resurrectVideoOnScreenShareEnd = true;
1606+
it('should stop screen share track even if no sender found', async () => {
15861607
session._screenShareStream = new MockStream({ video: true });
1608+
const screenTrack = session._screenShareStream._tracks[0];
1609+
1610+
jest.spyOn(session.pc, 'getSenders').mockReturnValue([]);
15871611

15881612
await handler.stopScreenShare(session);
15891613

1590-
expect(videoMuteSpy).toHaveBeenNthCalledWith(1, session, { conversationId: session.conversationId, mute: false }, true);
1591-
expect(videoMuteSpy).toHaveBeenNthCalledWith(2, session, { conversationId: session.conversationId, mute: true }, false);
1592-
expect(session._screenShareStream._tracks[0].stop).toHaveBeenCalled();
1614+
expect(removeMediaSpy).not.toHaveBeenCalled();
1615+
expect(screenTrack.stop).toHaveBeenCalled();
15931616
expect(mockSessionManager.webrtcSessions.notifyScreenShareStop).toHaveBeenCalled();
1594-
jest.resetAllMocks();
15951617
});
15961618
});
15971619

0 commit comments

Comments
 (0)