Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VIH-10689 Pause Resume audio recording #2271

Merged
merged 43 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
2e6001c
Created a new audio-recording.service.ts and shifted judge waiting ro…
will-craig Oct 2, 2024
259c7bc
New event hub action created
will-craig Oct 4, 2024
5a3e1ea
Added new event hup action
will-craig Oct 4, 2024
ff1cb3e
lint
will-craig Oct 4, 2024
c9c8429
test
will-craig Oct 4, 2024
a4340cb
lint
will-craig Oct 4, 2024
3503c62
Sonarcloud complaints
will-craig Oct 4, 2024
7ac7a1b
Merge branch 'master' into feature/VIH-10689--event-hub-changes
will-craig Oct 4, 2024
68f970d
Merge branch 'master' into feature/VIH-10689--event-hub-changes
will-craig Oct 4, 2024
109c8ab
lint
will-craig Oct 7, 2024
1f79c46
Merge branch 'feature/VIH-10689--event-hub-changes' of github.com:hmc…
will-craig Oct 7, 2024
a5e6463
typo
will-craig Oct 7, 2024
4a9fb6c
linting ...again
will-craig Oct 7, 2024
69bb1fc
Merge branch 'refs/heads/feature/VIH-10689--event-hub-changes' into f…
will-craig Oct 7, 2024
72f8616
Merge branch 'refs/heads/master' into feature/VIH-10689--judge-starts…
will-craig Oct 7, 2024
fb1463c
AudioRecordingPaused event hub changes
will-craig Oct 8, 2024
cb3dadb
AudioRecordingPaused event hub changes
will-craig Oct 8, 2024
3de5c6a
Naming convention updated
will-craig Oct 8, 2024
0912fea
Merge branch 'refs/heads/master' into feature/VIH-10689--event-hub-ch…
will-craig Oct 8, 2024
74bbea9
updated params
will-craig Oct 8, 2024
348bd1c
Typo
will-craig Oct 8, 2024
de44b53
Merge branch 'refs/heads/master' into feature/VIH-10689--judge-starts…
will-craig Oct 9, 2024
2f21ddd
Addec condownComplete and wowzaParticipants to the conference state s…
will-craig Oct 9, 2024
346023a
added deletion of wowza participant to store
will-craig Oct 9, 2024
f548926
Merge branch 'refs/heads/master' into feature/VIH-10689--event-hub-ch…
will-craig Oct 9, 2024
2c59dce
Updated audioRecording event to take bool state
will-craig Oct 9, 2024
1925866
Merge branch 'refs/heads/feature/VIH-10689--event-hub-changes' into f…
will-craig Oct 9, 2024
4712fa3
Merge branch 'refs/heads/master' into feature/VIH-10689--judge-starts…
will-craig Oct 10, 2024
3515c5f
Added conference store to audio-recording.service.ts
will-craig Oct 10, 2024
86789fa
VIH-10689 only display wowza pause button when wowza is connected
Oct 10, 2024
d110ca1
Updated tests
will-craig Oct 14, 2024
794a7d0
Merge branch 'refs/heads/master' into feature/VIH-10689--judge-starts…
will-craig Oct 14, 2024
e83e2f5
missing property
will-craig Oct 14, 2024
1ae3c2b
code coverage
will-craig Oct 14, 2024
39a0905
Additional test coverage
will-craig Oct 15, 2024
03992b9
Merge branch 'master' into feature/VIH-10689--judge-starts-stops-reco…
will-craig Oct 15, 2024
525699b
additional conference store update
will-craig Oct 15, 2024
6152fa6
test name
will-craig Oct 15, 2024
bbcce06
restartactioned update added back in
will-craig Oct 15, 2024
32231eb
added distinctUntilChanged
will-craig Oct 16, 2024
81d99bc
Merge branch 'master' into feature/VIH-10689--judge-starts-stops-reco…
will-craig Oct 17, 2024
83286a6
Merge branch 'master' into feature/VIH-10689--judge-starts-stops-reco…
will-craig Oct 24, 2024
1211dc8
Merge branch 'master' into feature/VIH-10689--judge-starts-stops-reco…
will-craig Oct 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions VideoWeb/VideoWeb.Common/RequestTelemetry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ public void Initialize(ITelemetry telemetry)

private bool IsReadableBadRequest(Microsoft.ApplicationInsights.DataContracts.RequestTelemetry telemetry)
{
return _httpContextAccessor.HttpContext.Request.Body.CanRead
&& telemetry.ResponseCode == "400";
if (_httpContextAccessor?.HttpContext == null)
{
return false;
}
return _httpContextAccessor.HttpContext.Request.Body.CanRead && telemetry.ResponseCode == "400";
}
}
}
}
3 changes: 1 addition & 2 deletions VideoWeb/VideoWeb.EventHub/Hub/EventHubClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -373,8 +373,7 @@ public async Task SendAudioRestartAction(Guid conferenceId, Guid participantId)
}
catch (Exception ex)
{
logger.LogError(ex, "Error occured when updating other hosts in conference {ConferenceId}",
conferenceId);
logger.LogError(ex, "Error occured when updating other hosts in conference {ConferenceId}", conferenceId);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { AudioRecordingService } from './audio-recording.service';
import { VideoCallService } from '../waiting-space/services/video-call.service';
import { EventsService } from './events.service';
import { Subject } from 'rxjs';
import { AudioRecordingPauseStateMessage } from '../shared/models/audio-recording-pause-state-message';
import { VHPexipParticipant } from '../waiting-space/store/models/vh-conference';
import * as ConferenceSelectors from '../waiting-space/store/selectors/conference.selectors';
import {
globalConference,
initAllWRDependencies,
mockConferenceStore
} from '../waiting-space/waiting-room-shared/tests/waiting-room-base-setup';

describe('AudioRecordingService', () => {
let service: AudioRecordingService;
let videoCallServiceSpy: jasmine.SpyObj<VideoCallService>;
let eventServiceSpy: jasmine.SpyObj<EventsService>;
const audioStoppedMock$ = new Subject<AudioRecordingPauseStateMessage>();
const pexipParticipant: VHPexipParticipant = {
isRemoteMuted: false,
isSpotlighted: false,
handRaised: false,
pexipDisplayName: 'vh-wowza',
uuid: 'unique-identifier',
callTag: 'call-tag',
isAudioOnlyCall: true,
isVideoCall: false,
protocol: 'protocol-type',
receivingAudioMix: 'audio-mix',
sentAudioMixes: []
};

beforeEach(() => {
mockConferenceStore.overrideSelector(ConferenceSelectors.getWowzaParticipant, pexipParticipant);
videoCallServiceSpy = jasmine.createSpyObj('VideoCallService', [
'disconnectWowzaAgent',
'connectWowzaAgent',
'getWowzaAgentConnectionState'
]);
eventServiceSpy = jasmine.createSpyObj('EventsService', ['sendAudioRecordingPaused', 'getAudioPaused']);
eventServiceSpy.getAudioPaused.and.returnValue(audioStoppedMock$);

const loggerMock = jasmine.createSpyObj('Logger', ['debug']);

service = new AudioRecordingService(loggerMock, videoCallServiceSpy, eventServiceSpy, mockConferenceStore);
});

it('should be created', () => {
expect(service).toBeTruthy();
});

it('should get audio pause state', done => {
service.getAudioRecordingPauseState().subscribe(isPaused => {
expect(isPaused).toBeTrue();
done();
});
audioStoppedMock$.next({ conferenceId: globalConference.id, pauseState: true });
});

it('should get Wowza agent connection state', done => {
const participant: VHPexipParticipant = { uuid: 'wowzaUUID', isAudioOnlyCall: false } as VHPexipParticipant;
mockConferenceStore.overrideSelector(ConferenceSelectors.getWowzaParticipant, participant);

service.getWowzaAgentConnectionState().subscribe(isConnected => {
expect(isConnected).toBeFalse();
done();
});

mockConferenceStore.refreshState();
});

describe('functions', () => {
it('should stop recording', async () => {
service.conference = { id: 'conferenceId' } as any;
service.wowzaAgent = { uuid: 'wowzaUUID' } as any;
await service.stopRecording();
expect(eventServiceSpy.sendAudioRecordingPaused).toHaveBeenCalledWith('conferenceId', true);
expect(videoCallServiceSpy.disconnectWowzaAgent).toHaveBeenCalledWith('wowzaUUID');
});

describe('reconnectToWowza', () => {
it('should reconnect to Wowza', async () => {
const failedToConnectCallback = jasmine.createSpy('failedToConnectCallback');
service.conference = { id: 'conferenceId', audioRecordingIngestUrl: 'ingestUrl' } as any;
videoCallServiceSpy.connectWowzaAgent.and.callFake((url, callback) => {
callback({ status: 'success', result: ['newUUID'] });
});

await service.reconnectToWowza(failedToConnectCallback);
expect(service.restartActioned).toBeTrue();
expect(videoCallServiceSpy.connectWowzaAgent).toHaveBeenCalledWith('ingestUrl', jasmine.any(Function));
expect(eventServiceSpy.sendAudioRecordingPaused).toHaveBeenCalledWith('conferenceId', false);
expect(failedToConnectCallback).not.toHaveBeenCalled();
});

it('should call failedToConnectCallback if reconnect to Wowza fails', async () => {
const failedToConnectCallback = jasmine.createSpy('failedToConnectCallback');
service.conference = { id: 'conferenceId', audioRecordingIngestUrl: 'ingestUrl' } as any;
videoCallServiceSpy.connectWowzaAgent.and.callFake((url, callback) => {
callback({ status: 'failure' });
});

await service.reconnectToWowza(failedToConnectCallback);
expect(failedToConnectCallback).toHaveBeenCalled();
});

it('should clean up dial out connections', () => {
service.dialOutUUID = ['uuid1', 'uuid2'];
service.cleanupDialOutConnections();
expect(videoCallServiceSpy.disconnectWowzaAgent).toHaveBeenCalledWith('uuid1');
expect(videoCallServiceSpy.disconnectWowzaAgent).toHaveBeenCalledWith('uuid2');
expect(service.dialOutUUID.length).toBe(0);
});
});

it('should clean up subscriptions on destroy', () => {
const onDestroySpy = spyOn(service['onDestroy$'], 'next');
const onDestroyCompleteSpy = spyOn(service['onDestroy$'], 'complete');
service.cleanupSubscriptions();
expect(onDestroySpy).toHaveBeenCalled();
expect(onDestroyCompleteSpy).toHaveBeenCalled();
});
});

afterAll(() => {
mockConferenceStore.resetSelectors();
});

beforeAll(() => {
initAllWRDependencies();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { VideoCallService } from '../waiting-space/services/video-call.service';
import { EventsService } from './events.service';
import { Logger } from './logging/logger-base';
import { AudioRecordingPauseStateMessage } from '../shared/models/audio-recording-pause-state-message';
import { Store } from '@ngrx/store';
import { ConferenceState } from '../waiting-space/store/reducers/conference.reducer';
import * as ConferenceSelectors from '../waiting-space/store/selectors/conference.selectors';
import { VHConference, VHPexipParticipant } from '../waiting-space/store/models/vh-conference';
import { takeUntil } from 'rxjs/operators';

@Injectable({
providedIn: 'root'
})
export class AudioRecordingService {
loggerPrefix = '[AudioRecordingService]';
dialOutUUID = [];
restartActioned: boolean;
conference: VHConference;
wowzaAgent: VHPexipParticipant;

private readonly audioStopped$: Subject<boolean> = new Subject<boolean>();
private readonly wowzaAgentConnection$ = new Subject<boolean>();
private readonly onDestroy$ = new Subject<void>();

constructor(
private readonly logger: Logger,
private readonly videoCallService: VideoCallService,
private readonly eventService: EventsService,
conferenceStore: Store<ConferenceState>
) {
conferenceStore
.select(ConferenceSelectors.getActiveConference)
.pipe(takeUntil(this.onDestroy$))
.subscribe(conference => {
this.conference = conference;
});

conferenceStore
.select(ConferenceSelectors.getWowzaParticipant)
.pipe(takeUntil(this.onDestroy$))
.subscribe(participant => {
this.dialOutUUID.push(participant?.uuid);
this.wowzaAgent = participant;
if (participant?.isAudioOnlyCall) {
this.wowzaAgentConnection$.next(true);
} else {
this.wowzaAgentConnection$.next(false);
}
});

this.eventService.getAudioPaused().subscribe(async (message: AudioRecordingPauseStateMessage) => {
if (this.conference.id === message.conferenceId) {
this.audioStopped$.next(message.pauseState);
}
});
}

getWowzaAgentConnectionState(): Observable<boolean> {
return this.wowzaAgentConnection$.asObservable();
}

getAudioRecordingPauseState(): Observable<boolean> {
return this.audioStopped$.asObservable();
}

async stopRecording() {
await this.eventService.sendAudioRecordingPaused(this.conference.id, true);
this.videoCallService.disconnectWowzaAgent(this.wowzaAgent.uuid);
}

async reconnectToWowza(failedToConnectCallback: Function) {
this.restartActioned = true;
this.cleanupDialOutConnections();
this.videoCallService.connectWowzaAgent(this.conference.audioRecordingIngestUrl, async dialOutToWowzaResponse => {
if (dialOutToWowzaResponse.status === 'success') {
this.dialOutUUID.push(dialOutToWowzaResponse.result[0]);
await this.eventService.sendAudioRecordingPaused(this.conference.id, false);
} else {
failedToConnectCallback();
}
});
}

cleanupDialOutConnections() {
this.logger.debug(`${this.loggerPrefix} Cleaning up dial out connections, if any {dialOutUUID: ${this.dialOutUUID}}`);
this.dialOutUUID?.forEach(uuid => {
this.videoCallService.disconnectWowzaAgent(uuid);
});
this.dialOutUUID = [];
}

cleanupSubscriptions() {
this.onDestroy$.next();
this.onDestroy$.complete();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ export class EventsService {

CountdownFinished: (conferenceId: string) => {
this.logger.debug('[EventsService] - CountdownFinished received', conferenceId);
this.store.dispatch(ConferenceActions.countdownComplete({ conferenceId }));
this.hearingCountdownCompleteSubject.next(conferenceId);
},

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Subject } from 'rxjs';

import { AudioRecordingService } from '../../services/audio-recording.service';
import { VHConference, VHPexipParticipant } from '../../waiting-space/store/models/vh-conference';

const mockConference: VHConference = {
id: '',
audioRecordingIngestUrl: '',
caseName: '',
caseNumber: '',
duration: 0,
endpoints: undefined,
isVenueScottish: false,
participants: undefined,
scheduledDateTime: undefined,
status: undefined,
supplier: undefined
};
const mockWowzaAgent: VHPexipParticipant = {
handRaised: false,
isAudioOnlyCall: true,
isRemoteMuted: false,
isSpotlighted: false,
isVideoCall: false,
pexipDisplayName: 'vh-wowza',
protocol: '',
receivingAudioMix: '',
sentAudioMixes: undefined,
uuid: 'wowzaUUID',
callTag: 'callTag'
};

const getWowzaAgentConnectionState$ = new Subject<boolean>();
const getAudioRecordingPauseState$ = new Subject<boolean>();

export const audioRecordingServiceSpy = jasmine.createSpyObj<AudioRecordingService>(
'AudioRecordingService',
[
'getWowzaAgentConnectionState',
'getAudioRecordingPauseState',
'stopRecording',
'reconnectToWowza',
'cleanupDialOutConnections',
'cleanupSubscriptions'
],
{
conference: mockConference,
wowzaAgent: mockWowzaAgent,
dialOutUUID: [],
restartActioned: false,
loggerPrefix: '[AudioRecordingService]'
}
);

audioRecordingServiceSpy.getWowzaAgentConnectionState.and.returnValue(getWowzaAgentConnectionState$.asObservable());
audioRecordingServiceSpy.getAudioRecordingPauseState.and.returnValue(getAudioRecordingPauseState$.asObservable());
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ import { ConferenceState, initialState as initialConferenceState } from '../stor
import { createMockStore, MockStore } from '@ngrx/store/testing';
import { ConferenceActions } from '../store/actions/conference.actions';
import { take } from 'rxjs/operators';
import { NotificationToastrService } from '../services/notification-toastr.service';
import { audioRecordingServiceSpy } from '../../testing/mocks/mock-audio-recording.service';

describe('HearingControlsBaseComponent', () => {
const participantOneId = Guid.create().toString();
Expand Down Expand Up @@ -107,6 +109,7 @@ describe('HearingControlsBaseComponent', () => {
let clientSettingsResponse: ClientSettingsResponse;
let videoControlServiceSpy: jasmine.SpyObj<VideoControlService>;
let videoControlCacheSpy: jasmine.SpyObj<VideoControlCacheService>;
let notificationToastrServiceSpy: jasmine.SpyObj<NotificationToastrService>;

beforeEach(() => {
const initialState = initialConferenceState;
Expand Down Expand Up @@ -157,6 +160,7 @@ describe('HearingControlsBaseComponent', () => {

launchDarklyServiceSpy.getFlag.withArgs(FEATURE_FLAGS.wowzaKillButton, false).and.returnValue(of(true));
launchDarklyServiceSpy.getFlag.withArgs(FEATURE_FLAGS.vodafone, false).and.returnValue(of(false));
notificationToastrServiceSpy = jasmine.createSpyObj('NotificationToastrService', ['showError']);

component = new PrivateConsultationRoomControlsComponent(
videoCallService,
Expand All @@ -172,7 +176,9 @@ describe('HearingControlsBaseComponent', () => {
videoControlCacheSpy,
launchDarklyServiceSpy,
focusService,
mockStore
mockStore,
audioRecordingServiceSpy,
notificationToastrServiceSpy
);
conference = new ConferenceTestData().getConferenceNow();
component.participant = globalParticipant;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ <h1 #roomTitleLabel class="room-title-label">{{ getCaseNameAndNumber() }}</h1>
[isSupportedBrowserForNetworkHealth]="isSupportedBrowserForNetworkHealth"
[showConsultationControls]="showConsultationControls"
[unreadMessageCount]="unreadMessageCount"
[wowzaUUID]="wowzaAgent?.uuid"
[isChatVisible]="isChatVisible"
[areParticipantsVisible]="areParticipantsVisible"
(leaveConsultation)="leaveConsultation()"
Expand Down
Loading
Loading