Skip to content

Commit

Permalink
VIH-10946 simplify the device stream management and enable audio only…
Browse files Browse the repository at this point in the history
… toggle in the hearing room
  • Loading branch information
Shaed Parkar authored and shaed-parkar committed Oct 18, 2024
1 parent 1330a76 commit ccd1fa6
Show file tree
Hide file tree
Showing 16 changed files with 604 additions and 314 deletions.
293 changes: 161 additions & 132 deletions VideoWeb/VideoWeb/ClientApp/package-lock.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { VideoWebService } from 'src/app/services/api/video-web.service';
import { ConferenceResponse, Role, UserProfileResponse } from 'src/app/services/clients/api-client';
import { ErrorService } from 'src/app/services/error.service';
import { Logger } from 'src/app/services/logging/logger-base';
import { UserMediaStreamService } from 'src/app/services/user-media-stream.service';
import { pageUrls } from 'src/app/shared/page-url.constants';
import { ConferenceTestData } from 'src/app/testing/mocks/data/conference-test-data';
import { MockLogger } from 'src/app/testing/mocks/mock-logger';
Expand All @@ -15,6 +14,7 @@ import { Subject, throwError } from 'rxjs';
import { mockCamStream } from 'src/app/waiting-space/waiting-room-shared/tests/waiting-room-base-setup';
import { getSpiedPropertyGetter } from 'src/app/shared/jasmine-helpers/property-helpers';
import { UserMediaService } from 'src/app/services/user-media.service';
import { UserMediaStreamServiceV2 } from 'src/app/services/user-media-stream-v2.service';

describe('SwitchOnCameraMicrophoneComponent', () => {
let component: SwitchOnCameraMicrophoneComponent;
Expand All @@ -26,7 +26,7 @@ describe('SwitchOnCameraMicrophoneComponent', () => {
let profileService: jasmine.SpyObj<ProfileService>;
let activatedRoute: ActivatedRoute = <any>{ snapshot: { paramMap: convertToParamMap({ conferenceId: conference.id }) } };
let videoWebService: jasmine.SpyObj<VideoWebService>;
let userMediaStreamService: jasmine.SpyObj<UserMediaStreamService>;
let userMediaStreamService: jasmine.SpyObj<UserMediaStreamServiceV2>;
let errorService: jasmine.SpyObj<ErrorService>;
let userMediaServiceSpy: jasmine.SpyObj<UserMediaService>;
const logger: Logger = new MockLogger();
Expand All @@ -35,7 +35,11 @@ describe('SwitchOnCameraMicrophoneComponent', () => {
beforeEach(async () => {
conference = new ConferenceTestData().getConferenceDetailFuture();
activatedRoute = <any>{ snapshot: { paramMap: convertToParamMap({ conferenceId: conference.id }) } };
userMediaStreamService = jasmine.createSpyObj<UserMediaStreamService>('UserMediaStreamService', [], ['currentStream$']);
userMediaStreamService = jasmine.createSpyObj<UserMediaStreamServiceV2>(
'UserMediaStreamServiceV2',
['createAndPublishStream', 'closeCurrentStream'],
['currentStream$']
);
currentStreamSubject = new Subject<MediaStream>();

videoWebService = jasmine.createSpyObj<VideoWebService>('VideoWebService', [
Expand Down Expand Up @@ -163,7 +167,7 @@ describe('SwitchOnCameraMicrophoneComponent', () => {
});

it('should update mediaAccepted and userPrompted to true when request media', fakeAsync(() => {
getSpiedPropertyGetter(userMediaStreamService, 'currentStream$').and.returnValue(currentStreamSubject.asObservable());
getSpiedPropertyGetter(userMediaStreamService, 'currentStream$').and.returnValue(currentStreamSubject);

component.requestMedia();
currentStreamSubject.next(mockCamStream);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ import { vhContactDetails } from 'src/app/shared/contact-information';
import { pageUrls } from 'src/app/shared/page-url.constants';
import { ParticipantStatusBaseDirective } from 'src/app/on-the-day/models/participant-status-base';
import { ParticipantStatusUpdateService } from 'src/app/services/participant-status-update.service';
import { UserMediaStreamService } from 'src/app/services/user-media-stream.service';
import { first } from 'rxjs/operators';
import { UserMediaService } from 'src/app/services/user-media.service';
import { HearingRole } from 'src/app/waiting-space/models/hearing-role-model';
import { UserMediaStreamServiceV2 } from 'src/app/services/user-media-stream-v2.service';

@Component({
selector: 'app-switch-on-camera-microphone',
Expand Down Expand Up @@ -43,7 +42,7 @@ export class SwitchOnCameraMicrophoneComponent extends ParticipantStatusBaseDire
protected logger: Logger,
protected participantStatusUpdateService: ParticipantStatusUpdateService,
private userMediaService: UserMediaService,
private userMediaStreamService: UserMediaStreamService
private userMediaStreamService: UserMediaStreamServiceV2
) {
super(participantStatusUpdateService, logger);
this.userPrompted = false;
Expand Down Expand Up @@ -81,10 +80,14 @@ export class SwitchOnCameraMicrophoneComponent extends ParticipantStatusBaseDire

async requestMedia() {
this.userMediaService.initialise();
this.userMediaStreamService.currentStream$.pipe(first()).subscribe({
this.userMediaStreamService.currentStream$.pipe().subscribe({
next: stream => {
if (!stream || !stream.active) {
return;
}
this.mediaAccepted = true;
this.userPrompted = true;
this.userMediaStreamService.closeCurrentStream();
},
error: error => {
this.mediaAccepted = false;
Expand All @@ -102,6 +105,7 @@ export class SwitchOnCameraMicrophoneComponent extends ParticipantStatusBaseDire
);
}
});
this.userMediaStreamService.createAndPublishStream();
}

goVideoTest() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { Subject } from 'rxjs';
import { getSpiedPropertyGetter } from '../shared/jasmine-helpers/property-helpers';
import { Logger } from './logging/logger-base';
import { NoSleepService } from './no-sleep.service';
import { UserMediaStreamService } from './user-media-stream.service';
import { UserMediaStreamServiceV2 } from './user-media-stream-v2.service';

describe('NoSleepService', () => {
let service: NoSleepService;

let userMediaStreamServiceSpy: jasmine.SpyObj<UserMediaStreamService>;
let userMediaStreamServiceSpy: jasmine.SpyObj<UserMediaStreamServiceV2>;
let renderer2FactorySpy: jasmine.SpyObj<RendererFactory2>;
let renderer2Spy: jasmine.SpyObj<Renderer2>;
let deviceServiceSpy: jasmine.SpyObj<DeviceDetectorService>;
Expand All @@ -24,7 +24,7 @@ describe('NoSleepService', () => {

beforeEach(() => {
currentStreamSubject = new Subject<MediaStream>();
userMediaStreamServiceSpy = jasmine.createSpyObj<UserMediaStreamService>([], ['currentStream$']);
userMediaStreamServiceSpy = jasmine.createSpyObj<UserMediaStreamServiceV2>([], ['currentStream$']);
getSpiedPropertyGetter(userMediaStreamServiceSpy, 'currentStream$').and.returnValue(currentStreamSubject.asObservable());

videoElementSpy = jasmine.createSpyObj<HTMLVideoElement>(['play', 'setAttribute'], ['style', 'parentElement']);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { DeviceDetectorService } from 'ngx-device-detector';
import { Observable, Subject } from 'rxjs';
import { take } from 'rxjs/operators';
import { Logger } from './logging/logger-base';
import { UserMediaStreamService } from './user-media-stream.service';
import { UserMediaStreamServiceV2 } from './user-media-stream-v2.service';

@Injectable({
providedIn: 'root'
Expand All @@ -16,7 +16,7 @@ export class NoSleepService {
private touchStartSubject = new Subject<void>();

constructor(
private userMediaStreamService: UserMediaStreamService,
private userMediaStreamService: UserMediaStreamServiceV2,
renderer2Factory: RendererFactory2,
private deviceService: DeviceDetectorService,
private document: Document,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { Guid } from 'guid-typescript';
import { getSpiedPropertyGetter } from '../shared/jasmine-helpers/property-helpers';
import { UserMediaDevice } from '../shared/models/user-media-device';
import { UserMediaStreamServiceV2 } from './user-media-stream-v2.service';
import { Logger } from './logging/logger-base';
import { UserMediaService } from './user-media.service';
import { AudioOnlyImageService } from './audio-only-image.service';
import { MediaStreamService } from './media-stream.service';
import { of, ReplaySubject } from 'rxjs';
import { fakeAsync, tick } from '@angular/core/testing';

describe('UserMediaStreamServiceV2', () => {
const mediaStreamBuilder = (device: UserMediaDevice) => {
const stream = jasmine.createSpyObj<MediaStream>(['addTrack', 'removeTrack', 'getTracks', 'getVideoTracks', 'getAudioTracks']);
const track = jasmine.createSpyObj<MediaStreamTrack>(['stop'], ['label', 'id']);

getSpiedPropertyGetter(track, 'label').and.returnValue(device.label);
getSpiedPropertyGetter(track, 'id').and.returnValue(Guid.create().toString());

stream.getTracks.and.returnValue([track]);
stream.getVideoTracks.and.returnValue([track]);
stream.getAudioTracks.and.returnValue([track]);

return stream;
};

let cameraOneDevice: UserMediaDevice;
let cameraOneStream: jasmine.SpyObj<MediaStream>;
let audioOnlyImageDevice: UserMediaDevice;
let audioOnlyImageStream: jasmine.SpyObj<MediaStream>;
let cameraTwoDevice: UserMediaDevice;
let cameraTwoStream: jasmine.SpyObj<MediaStream>;
let microphoneOneDevice: UserMediaDevice;
let microphoneOneStream: jasmine.SpyObj<MediaStream>;
let microphoneTwoDevice: UserMediaDevice;
let microphoneTwoStream: jasmine.SpyObj<MediaStream>;

let emptyStream: MediaStream;
let combinedStream: MediaStream;

let loggerSpy: jasmine.SpyObj<Logger>;
let userMediaServiceSpy: jasmine.SpyObj<UserMediaService>;
let mediaStreamServiceSpy: jasmine.SpyObj<MediaStreamService>;
let audioOnlyImageServiceSpy: jasmine.SpyObj<AudioOnlyImageService>;

let activeCameraDeviceSubject: ReplaySubject<UserMediaDevice>;
let activeMicrophoneDeviceSubject: ReplaySubject<UserMediaDevice>;
let isAudioOnlySubject: ReplaySubject<boolean>;

let sut: UserMediaStreamServiceV2;

beforeEach(() => {
cameraOneDevice = new UserMediaDevice('Camera 1', Guid.create().toString(), 'videoinput', '');
cameraOneStream = mediaStreamBuilder(cameraOneDevice);
audioOnlyImageDevice = new UserMediaDevice('Audio Only', Guid.create().toString(), 'videoinput', '');
audioOnlyImageStream = mediaStreamBuilder(audioOnlyImageDevice);
cameraTwoDevice = new UserMediaDevice('Camera 2', Guid.create().toString(), 'videoinput', '');
cameraTwoStream = mediaStreamBuilder(cameraTwoDevice);
microphoneOneDevice = new UserMediaDevice('Microphone 1', Guid.create().toString(), 'audioinput', '');
microphoneOneStream = mediaStreamBuilder(microphoneOneDevice);
microphoneTwoDevice = new UserMediaDevice('Microphone 2', Guid.create().toString(), 'audioinput', '');
microphoneTwoStream = mediaStreamBuilder(microphoneTwoDevice);

emptyStream = new MediaStream();
combinedStream = new MediaStream();

loggerSpy = jasmine.createSpyObj<Logger>(['debug', 'info', 'warn', 'error']);

activeCameraDeviceSubject = new ReplaySubject<UserMediaDevice>(1);
activeMicrophoneDeviceSubject = new ReplaySubject<UserMediaDevice>(1);
isAudioOnlySubject = new ReplaySubject<boolean>(1);
userMediaServiceSpy = jasmine.createSpyObj<UserMediaService>(
['initialise'],
['isAudioOnly$', 'activeVideoDevice$', 'activeMicrophoneDevice$']
);

getSpiedPropertyGetter(userMediaServiceSpy, 'activeVideoDevice$').and.returnValue(activeCameraDeviceSubject.asObservable());
getSpiedPropertyGetter(userMediaServiceSpy, 'activeMicrophoneDevice$').and.returnValue(
activeMicrophoneDeviceSubject.asObservable()
);
getSpiedPropertyGetter(userMediaServiceSpy, 'isAudioOnly$').and.returnValue(isAudioOnlySubject.asObservable());

mediaStreamServiceSpy = jasmine.createSpyObj<MediaStreamService>(['initialiseNewStream', 'getStreamForCam', 'getStreamForMic']);
mediaStreamServiceSpy.initialiseNewStream.withArgs([]).and.returnValue(emptyStream);
mediaStreamServiceSpy.getStreamForCam.withArgs(cameraOneDevice).and.returnValue(of(cameraOneStream));
mediaStreamServiceSpy.getStreamForCam.withArgs(cameraTwoDevice).and.returnValue(of(cameraTwoStream));

mediaStreamServiceSpy.getStreamForMic.withArgs(microphoneOneDevice).and.returnValue(of(microphoneOneStream));
mediaStreamServiceSpy.getStreamForMic.withArgs(microphoneTwoDevice).and.returnValue(of(microphoneTwoStream));

audioOnlyImageServiceSpy = jasmine.createSpyObj<AudioOnlyImageService>(['getAudioOnlyImageStream']);
audioOnlyImageServiceSpy.getAudioOnlyImageStream.and.returnValue(of(audioOnlyImageStream));

sut = new UserMediaStreamServiceV2(loggerSpy, userMediaServiceSpy, mediaStreamServiceSpy, audioOnlyImageServiceSpy);
});

it('should capture media devices, audio only state and create a stream', fakeAsync(() => {
activeCameraDeviceSubject.next(cameraOneDevice);
activeMicrophoneDeviceSubject.next(microphoneOneDevice);

mediaStreamServiceSpy.initialiseNewStream
.withArgs([...microphoneOneStream.getAudioTracks(), ...cameraOneStream.getVideoTracks()])
.and.returnValue(combinedStream);
isAudioOnlySubject.next(false);

tick();

expect(sut.currentStream).toBe(combinedStream);

expect(mediaStreamServiceSpy.getStreamForCam).toHaveBeenCalledWith(cameraOneDevice);
expect(mediaStreamServiceSpy.getStreamForMic).toHaveBeenCalledWith(microphoneOneDevice);
expect(audioOnlyImageServiceSpy.getAudioOnlyImageStream).not.toHaveBeenCalled();

sut.currentStream$.subscribe(stream => {
expect(stream).toBe(combinedStream);
});
}));

describe('audioOnly enabled', () => {
it('should create an audio only stream and no camera image', fakeAsync(() => {
const audioOnlyStream = new MediaStream();
mediaStreamServiceSpy.initialiseNewStream
.withArgs([...microphoneOneStream.getAudioTracks(), ...audioOnlyImageStream.getVideoTracks()])
.and.returnValue(audioOnlyStream);

activeCameraDeviceSubject.next(null);
activeMicrophoneDeviceSubject.next(microphoneOneDevice);
isAudioOnlySubject.next(true);

tick();

expect(sut.currentStream).toBe(audioOnlyStream);

expect(mediaStreamServiceSpy.getStreamForCam).not.toHaveBeenCalled();
expect(mediaStreamServiceSpy.getStreamForMic).toHaveBeenCalledWith(microphoneOneDevice);
expect(audioOnlyImageServiceSpy.getAudioOnlyImageStream).toHaveBeenCalled();

sut.currentStream$.subscribe(stream => {
expect(stream).toBe(audioOnlyStream);
});
}));

it('should create a no camera image stream only when no microphone detected', fakeAsync(() => {
sut.currentStream = combinedStream;

const imageOnlyStream = new MediaStream();
mediaStreamServiceSpy.initialiseNewStream
.withArgs([...emptyStream.getAudioTracks(), ...audioOnlyImageStream.getVideoTracks()])
.and.returnValue(imageOnlyStream);

activeCameraDeviceSubject.next(null);
activeMicrophoneDeviceSubject.next(null);
isAudioOnlySubject.next(true);

tick();

expect(sut.currentStream).toBe(imageOnlyStream);

expect(mediaStreamServiceSpy.getStreamForCam).not.toHaveBeenCalled();
expect(mediaStreamServiceSpy.getStreamForMic).not.toHaveBeenCalled();
expect(audioOnlyImageServiceSpy.getAudioOnlyImageStream).toHaveBeenCalled();

sut.currentStream$.subscribe(stream => {
expect(stream).toBe(imageOnlyStream);
});
}));
});

describe('audioOnly disabled', () => {
it('should create an empty stream when no camera or microphone detected', fakeAsync(() => {
activeCameraDeviceSubject.next(null);
activeMicrophoneDeviceSubject.next(null);
isAudioOnlySubject.next(false);

tick();

expect(sut.currentStream).toBe(emptyStream);

expect(mediaStreamServiceSpy.getStreamForCam).not.toHaveBeenCalled();
expect(mediaStreamServiceSpy.getStreamForMic).not.toHaveBeenCalled();
expect(audioOnlyImageServiceSpy.getAudioOnlyImageStream).not.toHaveBeenCalled();

sut.currentStream$.subscribe(stream => {
expect(stream).toBe(emptyStream);
});
}));
});

describe('closeCurrentStream', () => {
it('should do nothing is there is no current stream', fakeAsync(() => {
sut.currentStream = combinedStream;

sut.closeCurrentStream();
tick();

sut.currentStream$.subscribe(stream => {
expect(stream).toBeNull();
});

expect(sut.currentStream).toBeNull();
}));
});
});
Loading

0 comments on commit ccd1fa6

Please sign in to comment.