зеркало из
1
0
Форкнуть 0

Calling Declarative Transcription Notice (#190)

* Add CallingDeclarative transcription

* Add unit tests

* Change files

* Revise changelog message

* Address code review comments

* Fix build break

* Fix comment broken by merge

* Update api doc
This commit is contained in:
allenplusplus 2021-05-04 18:21:42 -07:00 коммит произвёл GitHub
Родитель 6168a8007a
Коммит e476866319
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
13 изменённых файлов: 222 добавлений и 31 удалений

Просмотреть файл

@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "Add transcription notice capability to Calling Declarative",
"packageName": "@azure/acs-calling-declarative",
"email": "allenhwang@microsoft.com",
"dependentChangeType": "patch"
}

Просмотреть файл

@ -36,6 +36,7 @@ export interface Call {
remoteParticipantsEnded: Map<string, RemoteParticipant>;
startTime: Date;
state: CallState;
transcription: TranscriptionCallFeature;
}
// @public
@ -110,6 +111,11 @@ export interface RemoteVideoStream {
videoStreamRendererView: VideoStreamRendererView | undefined;
}
// @public
export interface TranscriptionCallFeature {
isTranscriptionActive: boolean;
}
// @public
export interface VideoStreamRendererView {
isMirrored: boolean;

Просмотреть файл

@ -2,13 +2,17 @@
import {
Call,
CallAgent,
CallFeatureFactoryType,
CollectionUpdatedEvent,
GroupChatCallLocator,
GroupLocator,
IncomingCallEvent,
JoinCallOptions,
MeetingLocator,
StartCallOptions
RecordingCallFeature,
StartCallOptions,
TranscriptionCallFeature,
TransferCallFeature
} from '@azure/communication-calling';
import { CommunicationUserIdentifier, PhoneNumberIdentifier, UnknownIdentifier } from '@azure/communication-common';
import EventEmitter from 'events';
@ -22,12 +26,29 @@ import {
MockCall,
MockIncomingCall,
mockoutObjectFreeze,
MockRecordingCallFeatureImpl,
MockTranscriptionCallFeatureImpl,
MockTransferCallFeatureImpl,
waitWithBreakCondition
} from './TestUtils';
mockoutObjectFreeze();
jest.mock('@azure/communication-calling');
jest.mock('@azure/communication-calling', () => {
return {
Features: {
get Recording(): CallFeatureFactoryType<RecordingCallFeature> {
return MockRecordingCallFeatureImpl;
},
get Transfer(): CallFeatureFactoryType<TransferCallFeature> {
return MockTransferCallFeatureImpl;
},
get Transcription(): CallFeatureFactoryType<TranscriptionCallFeature> {
return MockTranscriptionCallFeatureImpl;
}
}
};
});
const mockRemoteParticipantId = 'a';
const mockCallId = 'b';

Просмотреть файл

@ -95,6 +95,9 @@ class ProxyCallAgent implements ProxyHandler<CallAgent> {
private addCall = (call: Call): void => {
this._callSubscribers.get(call)?.unsubscribe();
// For API extentions we need to have the call in the state when we are subscribing as we may want to update the
// state during the subscription process in the subscriber so we add the call to state before subscribing.
this._context.setCall(convertSdkCallToDeclarativeCall(call));
this._callSubscribers.set(call, new CallSubscriber(call, this._context, this._internalContext));
};

Просмотреть файл

@ -5,6 +5,7 @@ import {
CallClient,
CallFeatureFactoryType,
CreateViewOptions,
Features,
LocalVideoStream,
RecordingCallFeature,
RemoteVideoStream,
@ -28,8 +29,7 @@ import {
MockRecordingCallFeatureImpl,
MockRemoteParticipant,
MockTranscriptionCallFeatureImpl,
MockTransferCallFeatureImpl,
MOCK_RECORDING_NAME
MockTransferCallFeatureImpl
} from './TestUtils';
mockoutObjectFreeze();
@ -824,7 +824,9 @@ describe('declarative call client', () => {
createClientAndAgentMocks(testData);
createDeclarativeClient(testData);
const mockCall = createMockCall(mockCallId);
mockCall.api = createMockApiFeatures(true, new Map<string, any>());
const featureCache = new Map<any, any>();
featureCache.set(Features.Recording, addMockEmitter({ name: 'Default', isRecordingActive: true }));
mockCall.api = createMockApiFeatures(featureCache);
await createMockCallAndEmitCallsUpdated(testData, undefined, mockCall);
await waitWithBreakCondition(
@ -834,13 +836,31 @@ describe('declarative call client', () => {
expect(testData.declarativeCallClient.state.calls.get(mockCallId)?.recording.isRecordingActive).toBe(true);
});
test('should detect if call already has transcription active', async () => {
const testData = {} as TestData;
createClientAndAgentMocks(testData);
createDeclarativeClient(testData);
const mockCall = createMockCall(mockCallId);
const featureCache = new Map<any, any>();
featureCache.set(Features.Transcription, addMockEmitter({ name: 'Default', isTranscriptionActive: true }));
mockCall.api = createMockApiFeatures(featureCache);
await createMockCallAndEmitCallsUpdated(testData, undefined, mockCall);
await waitWithBreakCondition(
() => testData.declarativeCallClient.state.calls.get(mockCallId)?.transcription.isTranscriptionActive === true
);
expect(testData.declarativeCallClient.state.calls.get(mockCallId)?.transcription.isTranscriptionActive).toBe(true);
});
test('should detect recording changes in call', async () => {
const testData = {} as TestData;
createClientAndAgentMocks(testData);
createDeclarativeClient(testData);
const mockCall = createMockCall(mockCallId);
const featureCache = new Map<string, any>();
mockCall.api = createMockApiFeatures(true, featureCache);
const featureCache = new Map<any, any>();
featureCache.set(Features.Recording, addMockEmitter({ name: 'Default', isRecordingActive: true }));
mockCall.api = createMockApiFeatures(featureCache);
await createMockCallAndEmitCallsUpdated(testData, undefined, mockCall);
await waitWithBreakCondition(
@ -849,7 +869,7 @@ describe('declarative call client', () => {
expect(testData.declarativeCallClient.state.calls.get(mockCallId)?.recording.isRecordingActive).toBe(true);
const recording = featureCache.get(MOCK_RECORDING_NAME);
const recording = featureCache.get(Features.Recording);
recording.isRecordingActive = false;
recording.emitter.emit('isRecordingActiveChanged');
@ -860,19 +880,49 @@ describe('declarative call client', () => {
expect(testData.declarativeCallClient.state.calls.get(mockCallId)?.recording.isRecordingActive).toBe(false);
});
test('should detect transcription changes in call', async () => {
const testData = {} as TestData;
createClientAndAgentMocks(testData);
createDeclarativeClient(testData);
const mockCall = createMockCall(mockCallId);
const featureCache = new Map<any, any>();
featureCache.set(Features.Transcription, addMockEmitter({ name: 'Default', isTranscriptionActive: true }));
mockCall.api = createMockApiFeatures(featureCache);
await createMockCallAndEmitCallsUpdated(testData, undefined, mockCall);
await waitWithBreakCondition(
() => testData.declarativeCallClient.state.calls.get(mockCallId)?.transcription.isTranscriptionActive === true
);
expect(testData.declarativeCallClient.state.calls.get(mockCallId)?.transcription.isTranscriptionActive).toBe(true);
const transcription = featureCache.get(Features.Transcription);
transcription.isTranscriptionActive = false;
transcription.emitter.emit('isTranscriptionActiveChanged');
await waitWithBreakCondition(
() => testData.declarativeCallClient.state.calls.get(mockCallId)?.transcription.isTranscriptionActive === false
);
expect(testData.declarativeCallClient.state.calls.get(mockCallId)?.transcription.isTranscriptionActive).toBe(false);
});
test('should unsubscribe to recording changes when call ended', async () => {
const testData = {} as TestData;
createClientAndAgentMocks(testData);
createDeclarativeClient(testData);
const mockCall = createMockCall(mockCallId);
const featureCache = new Map<string, any>();
mockCall.api = createMockApiFeatures(true, featureCache);
const featureCache = new Map<any, any>();
featureCache.set(Features.Recording, addMockEmitter({ name: 'Default', isRecordingActive: true }));
mockCall.api = createMockApiFeatures(featureCache);
await createMockCallAndEmitCallsUpdated(testData, undefined, mockCall);
await waitWithBreakCondition(
() => testData.declarativeCallClient.state.calls.get(mockCallId)?.recording.isRecordingActive === true
);
expect(() => testData.declarativeCallClient.state.calls.get(mockCallId)?.recording.isRecordingActive === true);
expect(testData.declarativeCallClient.state.calls.get(mockCallId)?.recording.isRecordingActive).toBe(true);
testData.mockCallAgent.calls = [];
@ -883,7 +933,35 @@ describe('declarative call client', () => {
await waitWithBreakCondition(() => testData.declarativeCallClient.state.calls.size === 0);
const recording = featureCache.get(MOCK_RECORDING_NAME);
const recording = featureCache.get(Features.Recording);
expect(recording.emitter.eventNames().length).toBe(0);
});
test('should unsubscribe to transcription changes when call ended', async () => {
const testData = {} as TestData;
createClientAndAgentMocks(testData);
createDeclarativeClient(testData);
const mockCall = createMockCall(mockCallId);
const featureCache = new Map<any, any>();
featureCache.set(Features.Transcription, addMockEmitter({ name: 'Default', isTranscriptionActive: true }));
mockCall.api = createMockApiFeatures(featureCache);
await createMockCallAndEmitCallsUpdated(testData, undefined, mockCall);
await waitWithBreakCondition(
() => testData.declarativeCallClient.state.calls.get(mockCallId)?.transcription.isTranscriptionActive === true
);
expect(testData.declarativeCallClient.state.calls.get(mockCallId)?.transcription.isTranscriptionActive).toBe(true);
testData.mockCallAgent.calls = [];
testData.mockCallAgent.emit('callsUpdated', {
added: [],
removed: [testData.mockCall]
});
await waitWithBreakCondition(() => testData.declarativeCallClient.state.calls.size === 0);
const transcription = featureCache.get(Features.Transcription);
expect(transcription.emitter.eventNames().length).toBe(0);
});
});

Просмотреть файл

@ -19,6 +19,16 @@ import {
UnknownIdentifierKind
} from '@azure/communication-common';
/**
* State only version of {@Link @azure/communication-calling#TranscriptionCallFeature}.
*/
export interface TranscriptionCallFeature {
/**
* Proxy of {@Link @azure/communication-calling#TranscriptionCallFeature.isTranscriptionActive}.
*/
isTranscriptionActive: boolean;
}
/**
* State only version of {@Link @azure/communication-calling#RecordingCallFeature}.
*/
@ -172,6 +182,10 @@ export interface Call {
* {@Link Converter.getRemoteParticipantKey} to {@Link @azure/communication-calling#RemoteParticipant}
*/
remoteParticipantsEnded: Map<string, RemoteParticipant>;
/**
* Proxy of {@Link @azure/communication-calling#TranscriptionCallFeature}.
*/
transcription: TranscriptionCallFeature;
/**
* Proxy of {@Link @azure/communication-calling#RecordingCallFeature}.
*/

Просмотреть файл

@ -78,6 +78,7 @@ export class CallContext {
existingCall.isScreenSharingOn = call.isScreenSharingOn;
existingCall.localVideoStreams = call.localVideoStreams;
existingCall.remoteParticipants = call.remoteParticipants;
existingCall.transcription.isTranscriptionActive = call.transcription.isTranscriptionActive;
existingCall.recording.isRecordingActive = call.recording.isRecordingActive;
// We don't update the startTime and endTime if we are updating an existing active call
} else {
@ -219,6 +220,17 @@ export class CallContext {
);
}
public setCallTranscriptionActive(callId: string, isTranscriptionActive: boolean): void {
this.setState(
produce(this._state, (draft: CallClientState) => {
const call = draft.calls.get(callId);
if (call) {
call.transcription.isTranscriptionActive = isTranscriptionActive;
}
})
);
}
public setLocalVideoStreamRendererView(callId: string, view: VideoStreamRendererView | undefined): void {
this.setState(
produce(this._state, (draft: CallClientState) => {

Просмотреть файл

@ -12,6 +12,7 @@ import { InternalCallContext } from './InternalCallContext';
import { ParticipantSubscriber } from './ParticipantSubscriber';
import { RecordingSubscriber } from './RecordingSubscriber';
import { stopRenderVideo } from './StreamUtils';
import { TranscriptionSubscriber } from './TranscriptionSubscriber';
/**
* Keeps track of the listeners assigned to a particular call because when we get an event from SDK, it doesn't tell us
@ -25,6 +26,7 @@ export class CallSubscriber {
private _internalContext: InternalCallContext;
private _participantSubscribers: Map<string, ParticipantSubscriber>;
private _recordingSubscriber: RecordingSubscriber;
private _transcriptionSubscriber: TranscriptionSubscriber;
constructor(call: Call, context: CallContext, internalContext: InternalCallContext) {
this._call = call;
@ -39,6 +41,12 @@ export class CallSubscriber {
this._call.api(Features.Recording)
);
this._transcriptionSubscriber = new TranscriptionSubscriber(
this._callIdRef,
this._context,
this._call.api(Features.Transcription)
);
this.subscribe();
}
@ -85,6 +93,7 @@ export class CallSubscriber {
stopRenderVideo(this._context, this._internalContext, this._callIdRef.callId, localVideoStreams[0]);
}
this._transcriptionSubscriber.unsubscribe();
this._recordingSubscriber.unsubscribe();
};

Просмотреть файл

@ -111,6 +111,7 @@ export function convertSdkCallToDeclarativeCall(call: SdkCall): DeclarativeCall
remoteParticipants: declarativeRemoteParticipants,
remoteParticipantsEnded: new Map<string, DeclarativeRemoteParticipant>(),
recording: { isRecordingActive: false },
transcription: { isTranscriptionActive: false },
startTime: new Date(),
endTime: undefined
};

Просмотреть файл

@ -73,6 +73,7 @@ function createMockCall(mockCallId: string): Call {
remoteParticipants: new Map<string, RemoteParticipant>(),
remoteParticipantsEnded: new Map<string, RemoteParticipant>(),
recording: { isRecordingActive: false },
transcription: { isTranscriptionActive: false },
startTime: new Date(),
endTime: undefined
};

Просмотреть файл

@ -4,7 +4,6 @@ import {
CallAgent,
CallApiFeature,
CallFeatureFactoryType,
Features,
IncomingCall,
LocalVideoStream,
PropertyChangedEvent,
@ -117,7 +116,7 @@ export function createMockCall(mockCallId: string): MockCall {
id: mockCallId,
remoteParticipants: [] as ReadonlyArray<RemoteParticipant>,
localVideoStreams: [] as ReadonlyArray<LocalVideoStream>,
api: createMockApiFeatures(false, new Map<string, any>())
api: createMockApiFeatures(new Map())
} as MockCall;
return addMockEmitter(mockCall);
}
@ -140,32 +139,34 @@ export function createMockRemoteVideoStream(mockIsAvailable: boolean): MockRemot
return addMockEmitter(mockRemoteVideoStream);
}
export const MOCK_RECORDING_NAME = 'Recording';
/**
* Create a mockApiFeatures function see {@Link @azure/communication-calling#Call.api}. If cache is passed in, will try
* to return existing cached TFeatures else create new TFeature, cache it, and then return it.
* Creates a function equivalent to Call.api. The api() generated will use the passed in cache to return the feature
* objects as defined in the cache. For any undefined feature not in cache, it will return a generic object. Containing
* properties of all features. Note that this generic object is instanciated every call whereas the cache objects are
* reused on repeated calls.
*
* @param isRecording
* @param cache
* @returns
*/
export function createMockApiFeatures(
isRecording: boolean,
cache: Map<string, any>
cache: Map<CallFeatureFactoryType<any>, CallApiFeature>
): <FeatureT extends CallApiFeature>(cls: CallFeatureFactoryType<FeatureT>) => FeatureT {
return <FeatureT extends CallApiFeature>(cls: CallFeatureFactoryType<FeatureT>): FeatureT => {
if (typeof cls === typeof Features.Recording) {
if (cache.has(MOCK_RECORDING_NAME)) {
return cache.get(MOCK_RECORDING_NAME);
}
const recording = addMockEmitter({
isRecordingActive: isRecording
});
cache.set(MOCK_RECORDING_NAME, recording);
return recording;
const feature = cache.get(cls);
if (feature) {
return feature as FeatureT;
} else {
throw new Error('Not implemented');
// Default one if none provided
const generic = addMockEmitter({
name: 'Default',
isRecordingActive: false,
isTranscriptionActive: false,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
transfer(target: TransferToParticipant, transferOptions?: TransferToParticipantOptions): Transfer {
throw new Error('Method not implemented.');
}
});
return generic;
}
};
}

Просмотреть файл

@ -0,0 +1,37 @@
// © Microsoft Corporation. All rights reserved.
import { TranscriptionCallFeature } from '@azure/communication-calling';
import { CallContext } from './CallContext';
import { CallIdRef } from './CallIdRef';
export class TranscriptionSubscriber {
private _callIdRef: CallIdRef;
private _context: CallContext;
private _transcription: TranscriptionCallFeature;
constructor(callIdRef: CallIdRef, context: CallContext, transcription: TranscriptionCallFeature) {
this._callIdRef = callIdRef;
this._context = context;
this._transcription = transcription;
// If transcription as already started when we joined the call, make sure it is reflected in state as there may not
// be an event for it.
if (this._transcription.isTranscriptionActive) {
this._context.setCallTranscriptionActive(this._callIdRef.callId, this._transcription.isTranscriptionActive);
}
this.subscribe();
}
private subscribe = (): void => {
this._transcription.on('isTranscriptionActiveChanged', this.isTranscriptionActiveChanged);
};
public unsubscribe = (): void => {
this._transcription.off('isTranscriptionActiveChanged', this.isTranscriptionActiveChanged);
};
private isTranscriptionActiveChanged = (): void => {
this._context.setCallTranscriptionActive(this._callIdRef.callId, this._transcription.isTranscriptionActive);
};
}

Просмотреть файл

@ -10,5 +10,6 @@ export type {
RemoteVideoStream,
RemoteParticipant,
VideoStreamRendererView,
RecordingCallFeature
RecordingCallFeature,
TranscriptionCallFeature
} from './CallClientState';