Skip to content

Commit 06b6cf3

Browse files
authored
[STREAM-1178] - Remove support for sending multiple tracks while screensharing. (#975)
1 parent dc391da commit 06b6cf3

4 files changed

Lines changed: 79 additions & 93 deletions

File tree

changelog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
44
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
55

66
# [Unreleased](https://github.com/MyPureCloud/genesys-cloud-webrtc-sdk/compare/v11.5.1...HEAD)
7+
### Changed
8+
* [STREAM-1178](https://inindca.atlassian.net/browse/STREAM-1178) - REVERT STREAM-825, removed ability to send multiple tracks when screensharing.
79

810
# [v11.5.1](https://github.com/MyPureCloud/genesys-cloud-webrtc-sdk/compare/v11.5.0...v11.5.1)
911
### Added

src/sessions/base-session-handler.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -477,8 +477,7 @@ 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-
// 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));
480+
promises.push(session.peerConnection.addTrack(t));
482481
});
483482
} else {
484483
const errMsg = 'Track based actions are required for this session but the client is not capable';

src/sessions/video-session-handler.ts

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -550,13 +550,6 @@ 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-
560553
async setVideoMute (session: IExtendedMediaSession, params: ISessionMuteRequest, skipServerUpdate?: boolean): Promise<void> {
561554
const replayMuteRequest = !!session.videoMuted === !!params.mute;
562555

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

570563
const userId = this.sdk._personDetails.id;
571564

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

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

582577
if (sender) {
583578
await this.removeMediaFromSession(session, sender);
584579
}
585580

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

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

594590
// if we are unmuting, we need to get a new camera track and add that to the session
595591
} else {
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 });
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 });
601595
return;
602596
}
603597

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

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

723-
// Add screen share track without replacing existing video track
724-
await this.addMediaToSession(session, stream);
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+
}
725722

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

753-
if (sender) {
754-
// Remove the screen share track without affecting other video tracks
755-
await this.removeMediaFromSession(session, sender);
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);
756765
}
757766

758767
track.stop();

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

Lines changed: 44 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,7 +1004,7 @@ describe('setInitialMuteStates', () => {
10041004
beforeEach(() => {
10051005
session = new MockSession() as any;
10061006
audioSender = { track: { kind: 'audio', enabled: true } };
1007-
videoSender = { track: { kind: 'video', enabled: true, id: 'camera-track-1' } };
1007+
videoSender = { track: { kind: 'video', enabled: true } };
10081008
});
10091009

10101010
it('should mute video', async () => {
@@ -1217,7 +1217,7 @@ describe('setVideoMute', () => {
12171217

12181218
await handler.setVideoMute(session, { conversationId: session.conversationId, mute: false });
12191219

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

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

13091309
session._outboundStream = {
13101310
addTrack: jest.fn(),
@@ -1325,7 +1325,7 @@ describe('setVideoMute', () => {
13251325
const stream = new MockStream(true);
13261326
const spy = jest.spyOn(mockSdk.media, 'startMedia').mockResolvedValue(stream as any);
13271327
jest.spyOn(mockSessionManager, 'getAllActiveSessions').mockReturnValue([{ id: session.id } as IExtendedMediaSession ]);
1328-
jest.spyOn(handler, 'addReplaceTrackToSession').mockResolvedValue();
1328+
jest.spyOn(handler, 'addMediaToSession').mockResolvedValue();
13291329

13301330
session._outboundStream = {
13311331
addTrack: jest.fn(),
@@ -1345,7 +1345,7 @@ describe('setVideoMute', () => {
13451345
const unmuteDeviceId = 'device-id';
13461346
const stream = new MockStream(true);
13471347
const spy = jest.spyOn(mockSdk.media, 'startMedia').mockResolvedValue(stream as any);
1348-
jest.spyOn(handler, 'addReplaceTrackToSession').mockResolvedValue();
1348+
jest.spyOn(handler, 'addMediaToSession').mockResolvedValue();
13491349

13501350
session._outboundStream = {
13511351
addTrack: jest.fn(),
@@ -1359,51 +1359,6 @@ describe('setVideoMute', () => {
13591359
expect(session.unmute).not.toHaveBeenCalled();
13601360
expect(stream.getVideoTracks()[0].stop).toHaveBeenCalled();
13611361
});
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-
});
14071362
});
14081363

14091364
describe('setAudioMute', () => {
@@ -1526,29 +1481,33 @@ describe('getSendersByTrackType', () => {
15261481

15271482
describe('startScreenShare', () => {
15281483
let displayMediaSpy: jest.SpyInstance<Promise<MediaStream>>;
1529-
let addMediaToSessionSpy: jest.SpyInstance<Promise<void>>;
1484+
let videoMuteSpy: jest.SpyInstance<Promise<void>>;
1485+
let addReplaceTrackToSession: jest.SpyInstance<Promise<any>>;
15301486
let session: VideoMediaSession;
15311487

15321488
beforeEach(() => {
15331489
displayMediaSpy = jest.spyOn(mockSdk.media, 'startDisplayMedia').mockResolvedValue(new MockStream({ video: true }) as any);
1534-
addMediaToSessionSpy = jest.spyOn(handler, 'addMediaToSession').mockResolvedValue();
1490+
videoMuteSpy = jest.spyOn(handler, 'setVideoMute').mockResolvedValue();
1491+
addReplaceTrackToSession = jest.spyOn(handler, 'addReplaceTrackToSession').mockResolvedValue();
15351492
session = new MockSession() as any;
15361493
});
15371494

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

15411498
expect(displayMediaSpy).toHaveBeenCalled();
1542-
expect(addMediaToSessionSpy).toHaveBeenCalled();
1499+
expect(videoMuteSpy).toHaveBeenCalled();
1500+
expect(addReplaceTrackToSession).toHaveBeenCalled();
15431501
expect(mockSessionManager.webrtcSessions.notifyScreenShareStart).toHaveBeenCalled();
15441502
});
15451503

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

15501508
expect(displayMediaSpy).toHaveBeenCalled();
1551-
expect(addMediaToSessionSpy).toHaveBeenCalled();
1509+
expect(videoMuteSpy).not.toHaveBeenCalled();
1510+
expect(addReplaceTrackToSession).toHaveBeenCalled();
15521511
expect(mockSessionManager.webrtcSessions.notifyScreenShareStart).toHaveBeenCalled();
15531512
});
15541513

@@ -1573,47 +1532,64 @@ describe('startScreenShare', () => {
15731532
});
15741533

15751534
describe('stopScreenShare', () => {
1535+
let videoMuteSpy;
15761536
let removeMediaSpy;
15771537
let session;
15781538

15791539
beforeEach(() => {
1540+
videoMuteSpy = jest.spyOn(handler, 'setVideoMute').mockResolvedValue();
15801541
removeMediaSpy = jest.spyOn(handler, 'removeMediaFromSession').mockResolvedValue();
15811542
session = new MockSession();
15821543
});
15831544

1584-
it('should do nothing if there is no active screen share', async () => {
1545+
it('should do nothing if there is no screenshare stream', async () => {
15851546
session._screenShareStream = null;
15861547

15871548
await handler.stopScreenShare(session);
15881549

1550+
expect(videoMuteSpy).not.toHaveBeenCalled();
15891551
expect(removeMediaSpy).not.toHaveBeenCalled();
15901552
expect(mockSessionManager.webrtcSessions.notifyScreenShareStop).not.toHaveBeenCalled();
15911553
});
15921554

1593-
it('should remove screen share track and stop it', async () => {
1555+
it('should stop screen share tracks and unmute video if resurrectVideoOnScreenShareEnd', async () => {
1556+
session._resurrectVideoOnScreenShareEnd = true;
15941557
session._screenShareStream = new MockStream({ video: true });
1595-
const screenTrack = session._screenShareStream._tracks[0];
1596-
1597-
jest.spyOn(session.pc, 'getSenders').mockReturnValue([{ track: screenTrack }]);
15981558

15991559
await handler.stopScreenShare(session);
16001560

1601-
expect(removeMediaSpy).toHaveBeenCalled();
1602-
expect(screenTrack.stop).toHaveBeenCalled();
1561+
expect(videoMuteSpy).toHaveBeenCalled();
1562+
expect(session._screenShareStream._tracks[0].stop).toHaveBeenCalled();
16031563
expect(mockSessionManager.webrtcSessions.notifyScreenShareStop).toHaveBeenCalled();
16041564
});
16051565

1606-
it('should stop screen share track even if no sender found', async () => {
1566+
it('should not unmute video if not resurrect', async () => {
1567+
session.resurrectVideoOnScreenShareEnd = false;
16071568
session._screenShareStream = new MockStream({ video: true });
1608-
const screenTrack = session._screenShareStream._tracks[0];
16091569

1610-
jest.spyOn(session.pc, 'getSenders').mockReturnValue([]);
1570+
const replaceSpy = jest.fn();
1571+
jest.spyOn(session.pc, 'getSenders').mockReturnValue([{ track: session._screenShareStream._tracks[0], replaceTrack: replaceSpy }]);
16111572

16121573
await handler.stopScreenShare(session);
16131574

1614-
expect(removeMediaSpy).not.toHaveBeenCalled();
1615-
expect(screenTrack.stop).toHaveBeenCalled();
1575+
expect(videoMuteSpy).not.toHaveBeenCalled();
1576+
expect(session._screenShareStream._tracks[0].stop).toHaveBeenCalled();
1577+
expect(replaceSpy).toHaveBeenCalled();
1578+
expect(mockSessionManager.webrtcSessions.notifyScreenShareStop).toHaveBeenCalled();
1579+
});
1580+
1581+
it('should toggle off of screen share AND keep video unmuted if something goes wrong when fetching device media', async () => {
1582+
videoMuteSpy = jest.spyOn(handler, 'setVideoMute').mockRejectedValueOnce({ message: 'Could not start video source' }).mockResolvedValueOnce();
1583+
session._resurrectVideoOnScreenShareEnd = true;
1584+
session._screenShareStream = new MockStream({ video: true });
1585+
1586+
await handler.stopScreenShare(session);
1587+
1588+
expect(videoMuteSpy).toHaveBeenNthCalledWith(1, session, { conversationId: session.conversationId, mute: false }, true);
1589+
expect(videoMuteSpy).toHaveBeenNthCalledWith(2, session, { conversationId: session.conversationId, mute: true }, false);
1590+
expect(session._screenShareStream._tracks[0].stop).toHaveBeenCalled();
16161591
expect(mockSessionManager.webrtcSessions.notifyScreenShareStop).toHaveBeenCalled();
1592+
jest.resetAllMocks();
16171593
});
16181594
});
16191595

0 commit comments

Comments
 (0)