diff --git a/change/@azure-acs-calling-declarative-c9b4d5fb-e71e-4f0c-adea-c585d352f41e.json b/change/@azure-acs-calling-declarative-c9b4d5fb-e71e-4f0c-adea-c585d352f41e.json new file mode 100644 index 00000000000..48968631bb6 --- /dev/null +++ b/change/@azure-acs-calling-declarative-c9b4d5fb-e71e-4f0c-adea-c585d352f41e.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Upgrade to Calling SDK version 1.0.1-beta.1", + "packageName": "@azure/acs-calling-declarative", + "email": "allenhwang@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@azure-communication-ui-5fb75cbb-dbda-4d69-93b6-cbeb87c4c0d0.json b/change/@azure-communication-ui-5fb75cbb-dbda-4d69-93b6-cbeb87c4c0d0.json new file mode 100644 index 00000000000..80df0fd8dd6 --- /dev/null +++ b/change/@azure-communication-ui-5fb75cbb-dbda-4d69-93b6-cbeb87c4c0d0.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Upgrade to Calling SDK version 1.0.1-beta.1", + "packageName": "@azure/communication-ui", + "email": "allenhwang@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index aecb10bea93..a02b28af03f 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -1,5 +1,7 @@ dependencies: '@azure/communication-administration': 1.0.0-beta.3 + '@azure/communication-calling': 1.0.1-beta.1 + '@azure/communication-common-beta3': /@azure/communication-common/1.0.0-beta.3 '@azure/communication-identity': 1.0.0-beta.4 '@azure/communication-signaling-2': /@azure/communication-signaling/1.0.0-beta.2 '@azure/core-http': 1.2.3 @@ -164,18 +166,13 @@ packages: node: '>=8.0.0' resolution: integrity: sha512-JqGWH19PZKtW49Y4RfyOxmm+iPLOYZ5xP8bQ8ZhoAJ9aDzfY/UEEbmjaVPYFdso4UVKgPP6y0lOmy73xpwKTVg== - /@azure/communication-calling/1.0.0-beta.2: - deprecated: This version is deprecated. Please update to the latest @azure/communication-calling@1.0.0-beta.9 - dev: false - resolution: - integrity: sha512-PF33xuq1FGYDd09+IiQKJ/g/ATQGx1GFLflXJ9V0cErrIvSnhwtqd2f5j/1CeJ6fqhqfZ8RzcOepV2R7gP4BtA== - /@azure/communication-calling/1.0.0-beta.9: + /@azure/communication-calling/1.0.1-beta.1: dependencies: - '@azure/communication-common': 1.0.0-beta.4 + '@azure/communication-common': 1.0.0 '@azure/logger': 1.0.2 dev: false resolution: - integrity: sha512-dQ2lIrB+XlNtYKSVUBleM17nQYdckqA369X0/WFq89DVRRHhjZ+nscayC006eBtubFbe0zYwpEZmZN4Il7C6kw== + integrity: sha512-jjkAcO+dFKl6fjdI62mgImF7VJ48vRs6PfR5TfuswZQ758k1bHxnJfi8T1jS8F5JcBJGEzoUsaFCQD8Ae2eFgg== /@azure/communication-chat/1.0.0-beta.3: dependencies: '@azure/abort-controller': 1.0.4 @@ -212,7 +209,7 @@ packages: node: '>=8.0.0' resolution: integrity: sha512-SLdpTkoJTE1LE5IVj/fJH7oYmWXNxmEmeMIHVvPoHAbEqvjOIN0DrG0m5D4afYUl3BgL3QQJ4hG+lDDbi1XT/w== - /@azure/communication-common/1.0.0-beta.3: + /@azure/communication-common/1.0.0: dependencies: '@azure/abort-controller': 1.0.4 '@azure/core-auth': 1.2.0 @@ -225,8 +222,8 @@ packages: engines: node: '>=8.0.0' resolution: - integrity: sha512-2Yxr/wrbObcy2xcEp9b4tBv8KhFEpieYfEhf+jSYzoqAfX49WUiCNBt2u2HmwSTm81sr4ppDS7w8qWioVpUgJw== - /@azure/communication-common/1.0.0-beta.4: + integrity: sha512-kBWnamOow0COBPHkkUKaQl4wVMlkOpTDFmvRRW+JN3+JIM9ca1l1wdZA3Q6XfpYdpqoI+tVICr3+3SxyF7ulxw== + /@azure/communication-common/1.0.0-beta.3: dependencies: '@azure/abort-controller': 1.0.4 '@azure/core-auth': 1.2.0 @@ -239,7 +236,7 @@ packages: engines: node: '>=8.0.0' resolution: - integrity: sha512-HPLwM6EaQ8Grc+smncr4t/Q/I0TnhAS3Ql/z9Zuju7hXDcSBCYjINmaYNQ76LCdhDQ62Akzye7vYak4eOuit9w== + integrity: sha512-2Yxr/wrbObcy2xcEp9b4tBv8KhFEpieYfEhf+jSYzoqAfX49WUiCNBt2u2HmwSTm81sr4ppDS7w8qWioVpUgJw== /@azure/communication-common/1.0.0-beta.5: dependencies: '@azure/abort-controller': 1.0.4 @@ -20218,8 +20215,8 @@ packages: integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw== file:projects/acs-calling-declarative.tgz_prettier@2.0.5+ts-node@9.1.1: dependencies: - '@azure/communication-calling': 1.0.0-beta.9 - '@azure/communication-common': 1.0.0-beta.4 + '@azure/communication-calling': 1.0.1-beta.1 + '@azure/communication-common': 1.0.0 '@microsoft/api-documenter': 7.12.12 '@microsoft/api-extractor': 7.13.2 '@types/jest': 26.0.20 @@ -20244,7 +20241,7 @@ packages: prettier: '*' ts-node: '*' resolution: - integrity: sha512-MY4NtNRjLTPuxGxYyxB7OUUWmrTfgUUuvpYjzVJ7DTnmdoerowBWSgbuT3wfnaLsrkYTeC/qizCvTty9LiiVjg== + integrity: sha512-6WDVzpnjmR2MwCtI1zhNSC+8t40zkB4OZZbi6nF+p/SMwtYGwjl0g8FVF7L9zYL7ENgf6Jdqxs9flYTQ7bMgoQ== tarball: file:projects/acs-calling-declarative.tgz version: 0.0.0 file:projects/acs-chat-declarative.tgz_prettier@2.0.5+ts-node@9.1.1: @@ -20328,8 +20325,8 @@ packages: file:projects/calling.tgz_570bf42e92a70fb216c5cd4d5ed668ef: dependencies: '@azure/communication-administration': 1.0.0-beta.3 - '@azure/communication-calling': 1.0.0-beta.2 - '@azure/communication-common': 1.0.0-beta.3 + '@azure/communication-calling': 1.0.1-beta.1 + '@azure/communication-common': 1.0.0 '@azure/core-http': 1.2.3 '@babel/core': 7.13.10 '@babel/preset-env': 7.13.10_@babel+core@7.13.10 @@ -20390,7 +20387,7 @@ packages: webpack: '*' webpack-cli: '*' resolution: - integrity: sha512-PYwlIGfcB/u+Jv4BePuPKciDw4bnKNnSzu8ayjoITq0Gt2v1SATcTJX9vaLzHIFKI4CRbOzu/KRFovJg757XCg== + integrity: sha512-cvgnfZYSK6tTMoat2cY5pUeEA4a8e3HMKUgiz1z6PIULBM/Wp8xP1NPzuwD5Q9PRIUujT+vYi7544Mk1q/TPFw== tarball: file:projects/calling.tgz version: 0.0.0 file:projects/chat.tgz_40b0c9f7a7a0fac66e4b6fcf06de2a42: @@ -20458,9 +20455,10 @@ packages: file:projects/communication-ui.tgz_webpack-cli@4.5.0: dependencies: '@azure/communication-administration': 1.0.0-beta.3 - '@azure/communication-calling': 1.0.0-beta.2 + '@azure/communication-calling': 1.0.1-beta.1 '@azure/communication-chat': 1.0.0-beta.4 - '@azure/communication-common': 1.0.0-beta.3 + '@azure/communication-common': 1.0.0 + '@azure/communication-common-beta3': /@azure/communication-common/1.0.0-beta.3 '@azure/communication-signaling': 1.0.0-beta.1 '@azure/communication-signaling-2': /@azure/communication-signaling/1.0.0-beta.2 '@azure/core-http': 1.2.3 @@ -20547,14 +20545,14 @@ packages: peerDependencies: webpack-cli: '*' resolution: - integrity: sha512-IAJnzwRH5gb4xIeXlVGGfFS8AmJNS1k6qOqJuplSutwM7OO3uzuWoumkn8R6NfFO7B/X2hP7RMuQUIEogXkszw== + integrity: sha512-nQJvypLutxWMSYZChumjynxphCXkwaA8m5aaL/gtin6w2UaZNGUhdCLlylY+bWbb8vwA/kkoInrrwwC3DcsCRw== tarball: file:projects/communication-ui.tgz version: 0.0.0 file:projects/one-to-one-call.tgz_570bf42e92a70fb216c5cd4d5ed668ef: dependencies: '@azure/communication-administration': 1.0.0-beta.3 - '@azure/communication-calling': 1.0.0-beta.2 - '@azure/communication-common': 1.0.0-beta.3 + '@azure/communication-calling': 1.0.1-beta.1 + '@azure/communication-common': 1.0.0 '@azure/core-http': 1.2.3 '@babel/core': 7.13.10 '@babel/preset-env': 7.13.10_@babel+core@7.13.10 @@ -20617,7 +20615,7 @@ packages: webpack: '*' webpack-cli: '*' resolution: - integrity: sha512-7Eei6TVL74VGNBmON8HLssHISzQr8G8ImvnnvLBOTTGUat72KR7QjO16GHMr4QUYlKFLFd/+hvFFiwjvu6u/hw== + integrity: sha512-mdOZ2vFNF5mBoIE/2Qj3lXgSOeH3BIzBd3dehMA64/0RD1pEWcnSm5VsqU2W6zbQAzSFA1x6Eion3jlIozBWTg== tarball: file:projects/one-to-one-call.tgz version: 0.0.0 file:projects/server.tgz: @@ -20673,8 +20671,8 @@ packages: file:projects/storybook.tgz_webpack-cli@4.5.0: dependencies: '@azure/communication-administration': 1.0.0-beta.3 - '@azure/communication-calling': 1.0.0-beta.2 - '@azure/communication-chat': 1.0.0-beta.3 + '@azure/communication-calling': 1.0.1-beta.1 + '@azure/communication-chat': 1.0.0-beta.4 '@azure/communication-common': 1.0.0-beta.3 '@azure/communication-signaling': 1.0.0-beta.1 '@azure/core-http': 1.2.3 @@ -20759,12 +20757,14 @@ packages: peerDependencies: webpack-cli: '*' resolution: - integrity: sha512-OGk5WmNz5SZQe7OyGHpnENmNpPJXnakYDoDsXioHAFVPDfvjXt/jLBa9EXlF0PjwW6samC4ZJqvRxNuU/IjhJw== + integrity: sha512-8SZsUIBIxRx+iaIbRGanCod+Qh25XQ3Ox0U729RkB4XXLJVMXq2odNRpr6lmiU7St1sDt+HejqmshxM6+5Z8Ig== tarball: file:projects/storybook.tgz version: 0.0.0 registry: '' specifiers: '@azure/communication-administration': 1.0.0-beta.3 + '@azure/communication-calling': 1.0.1-beta.1 + '@azure/communication-common-beta3': npm:@azure/communication-common@1.0.0-beta.3 '@azure/communication-identity': 1.0.0-beta.4 '@azure/communication-signaling-2': npm:@azure/communication-signaling@1.0.0-beta.2 '@azure/core-http': ^1.2.3 diff --git a/packages/acs-calling-declarative/package.json b/packages/acs-calling-declarative/package.json index 8fd4b163445..b44c00283d2 100644 --- a/packages/acs-calling-declarative/package.json +++ b/packages/acs-calling-declarative/package.json @@ -32,8 +32,8 @@ ] }, "dependencies": { - "@azure/communication-common": "1.0.0-beta.4", - "@azure/communication-calling": "1.0.0-beta.9", + "@azure/communication-common": "1.0.0", + "@azure/communication-calling": "1.0.1-beta.1", "events": "~3.3.0", "immer": "~8.0.1" }, diff --git a/packages/acs-calling-declarative/review/acs-calling-declarative.api.md b/packages/acs-calling-declarative/review/acs-calling-declarative.api.md index 0cf9b62b949..401b3b2f149 100644 --- a/packages/acs-calling-declarative/review/acs-calling-declarative.api.md +++ b/packages/acs-calling-declarative/review/acs-calling-declarative.api.md @@ -9,7 +9,6 @@ import { CallClient } from '@azure/communication-calling'; import { CallDirection } from '@azure/communication-calling'; import { CallEndReason } from '@azure/communication-calling'; import { CallerInfo } from '@azure/communication-calling'; -import { CallingApplicationKind } from '@azure/communication-common'; import { CallState } from '@azure/communication-calling'; import { CommunicationUserKind } from '@azure/communication-common'; import { DeviceAccess } from '@azure/communication-calling'; @@ -27,7 +26,7 @@ export interface Call { direction: CallDirection; endTime: Date | undefined; id: string; - isMicrophoneMuted: boolean; + isMuted: boolean; isScreenSharingOn: boolean; localVideoStreams: ReadonlyArray; remoteParticipants: Map; @@ -84,7 +83,7 @@ export interface LocalVideoStream { export interface RemoteParticipant { callEndReason?: CallEndReason; displayName?: string; - identifier: CommunicationUserKind | PhoneNumberKind | CallingApplicationKind | MicrosoftTeamsUserKind | UnknownIdentifierKind; + identifier: CommunicationUserKind | PhoneNumberKind | MicrosoftTeamsUserKind | UnknownIdentifierKind; isMuted: boolean; isSpeaking: boolean; state: RemoteParticipantState; diff --git a/packages/acs-calling-declarative/src/CallAgentDeclarative.test.ts b/packages/acs-calling-declarative/src/CallAgentDeclarative.test.ts index 9fda39c3435..c7edfd5fdd1 100644 --- a/packages/acs-calling-declarative/src/CallAgentDeclarative.test.ts +++ b/packages/acs-calling-declarative/src/CallAgentDeclarative.test.ts @@ -3,18 +3,14 @@ import { Call, CallAgent, CollectionUpdatedEvent, + GroupChatCallLocator, GroupLocator, IncomingCallEvent, JoinCallOptions, MeetingLocator, StartCallOptions } from '@azure/communication-calling'; -import { - CommunicationUserIdentifier, - PhoneNumberIdentifier, - CallingApplicationIdentifier, - UnknownIdentifier -} from '@azure/communication-common'; +import { CommunicationUserIdentifier, PhoneNumberIdentifier, UnknownIdentifier } from '@azure/communication-common'; import EventEmitter from 'events'; import { callAgentDeclaratify } from './CallAgentDeclarative'; import { CallContext, MAX_CALL_HISTORY_LENGTH } from './CallContext'; @@ -42,12 +38,7 @@ class MockCallAgent implements CallAgent { startCall( // eslint-disable-next-line @typescript-eslint/no-unused-vars - participants: ( - | CommunicationUserIdentifier - | PhoneNumberIdentifier - | CallingApplicationIdentifier - | UnknownIdentifier - )[], + participants: (CommunicationUserIdentifier | PhoneNumberIdentifier | UnknownIdentifier)[], // eslint-disable-next-line @typescript-eslint/no-unused-vars options?: StartCallOptions ): Call { @@ -57,6 +48,7 @@ class MockCallAgent implements CallAgent { return call; } join(groupLocator: GroupLocator, options?: JoinCallOptions): Call; + join(groupChatCallLocator: GroupChatCallLocator, options?: JoinCallOptions): Call; join(meetingLocator: MeetingLocator, options?: JoinCallOptions): Call; // eslint-disable-next-line @typescript-eslint/no-unused-vars join(meetingLocator: any, options?: any): Call { diff --git a/packages/acs-calling-declarative/src/CallClientState.ts b/packages/acs-calling-declarative/src/CallClientState.ts index a6319fce698..152d7982f60 100644 --- a/packages/acs-calling-declarative/src/CallClientState.ts +++ b/packages/acs-calling-declarative/src/CallClientState.ts @@ -12,7 +12,6 @@ import { VideoDeviceInfo } from '@azure/communication-calling'; import { - CallingApplicationKind, CommunicationUserKind, MicrosoftTeamsUserKind, PhoneNumberKind, @@ -58,12 +57,7 @@ export interface RemoteParticipant { /** * Proxy of {@Link @azure/communication-calling#RemoteParticipant.identifier}. */ - identifier: - | CommunicationUserKind - | PhoneNumberKind - | CallingApplicationKind - | MicrosoftTeamsUserKind - | UnknownIdentifierKind; + identifier: CommunicationUserKind | PhoneNumberKind | MicrosoftTeamsUserKind | UnknownIdentifierKind; /** * Proxy of {@Link @azure/communication-calling#RemoteParticipant.displayName}. */ @@ -115,9 +109,9 @@ export interface Call { */ direction: CallDirection; /** - * Proxy of {@Link @azure/communication-calling#Call.isMicrophoneMuted}. + * Proxy of {@Link @azure/communication-calling#Call.isMuted}. */ - isMicrophoneMuted: boolean; + isMuted: boolean; /** * Proxy of {@Link @azure/communication-calling#Call.isScreenSharingOn}. */ diff --git a/packages/acs-calling-declarative/src/CallContext.ts b/packages/acs-calling-declarative/src/CallContext.ts index 9bf3f5b9d12..5ab644de228 100644 --- a/packages/acs-calling-declarative/src/CallContext.ts +++ b/packages/acs-calling-declarative/src/CallContext.ts @@ -69,7 +69,7 @@ export class CallContext { existingCall.state = call.state; existingCall.callEndReason = call.callEndReason; existingCall.direction = call.direction; - existingCall.isMicrophoneMuted = call.isMicrophoneMuted; + existingCall.isMuted = call.isMuted; existingCall.isScreenSharingOn = call.isScreenSharingOn; existingCall.localVideoStreams = call.localVideoStreams; existingCall.remoteParticipants = call.remoteParticipants; @@ -196,7 +196,7 @@ export class CallContext { produce(this._state, (draft: CallClientState) => { const call = draft.calls.get(callId); if (call) { - call.isMicrophoneMuted = isMicrophoneMuted; + call.isMuted = isMicrophoneMuted; } }) ); diff --git a/packages/acs-calling-declarative/src/CallDeclarative.test.ts b/packages/acs-calling-declarative/src/CallDeclarative.test.ts index b8f41bd404b..20af83e6dfe 100644 --- a/packages/acs-calling-declarative/src/CallDeclarative.test.ts +++ b/packages/acs-calling-declarative/src/CallDeclarative.test.ts @@ -16,11 +16,10 @@ describe('declarative call', () => { mockCall.callerInfo = { identifier: { kind: 'communicationUser' } } as CallerInfo; mockCall.state = 'None'; mockCall.direction = 'Incoming'; - mockCall.isMicrophoneMuted = false; + mockCall.isMuted = false; mockCall.isScreenSharingOn = false; mockCall.localVideoStreams = []; mockCall.remoteParticipants = []; - mockCall.isMicrophoneMuted = false; mockCall.mute = () => { return Promise.resolve(); }; @@ -33,14 +32,14 @@ describe('declarative call', () => { const declarativeCall = callDeclaratify(mockCall, context); - mockCall.isMicrophoneMuted = true; + mockCall.isMuted = true; await declarativeCall.mute(); - expect(context.getState().calls.get(mockCallId)?.isMicrophoneMuted).toBe(true); + expect(context.getState().calls.get(mockCallId)?.isMuted).toBe(true); - mockCall.isMicrophoneMuted = false; + mockCall.isMuted = false; await declarativeCall.unmute(); - expect(context.getState().calls.get(mockCallId)?.isMicrophoneMuted).toBe(false); + expect(context.getState().calls.get(mockCallId)?.isMuted).toBe(false); }); }); diff --git a/packages/acs-calling-declarative/src/CallDeclarative.ts b/packages/acs-calling-declarative/src/CallDeclarative.ts index 140f550232f..616a09cabdf 100644 --- a/packages/acs-calling-declarative/src/CallDeclarative.ts +++ b/packages/acs-calling-declarative/src/CallDeclarative.ts @@ -22,14 +22,14 @@ class ProxyCall implements ProxyHandler { case 'mute': { return (): Promise => { return target.mute().then(() => { - this._context.setCallIsMicrophoneMuted(this._call.id, this._call.isMicrophoneMuted); + this._context.setCallIsMicrophoneMuted(this._call.id, this._call.isMuted); }); }; } case 'unmute': { return (): Promise => { return target.unmute().then(() => { - this._context.setCallIsMicrophoneMuted(this._call.id, this._call.isMicrophoneMuted); + this._context.setCallIsMicrophoneMuted(this._call.id, this._call.isMuted); }); }; } diff --git a/packages/acs-calling-declarative/src/Converter.ts b/packages/acs-calling-declarative/src/Converter.ts index d7dd8a2932a..fb82ad2ca27 100644 --- a/packages/acs-calling-declarative/src/Converter.ts +++ b/packages/acs-calling-declarative/src/Converter.ts @@ -7,7 +7,6 @@ import { IncomingCall as SdkIncomingCall } from '@azure/communication-calling'; import { - CallingApplicationKind, CommunicationUserKind, MicrosoftTeamsUserKind, PhoneNumberKind, @@ -61,14 +60,9 @@ export function convertSdkParticipantToDeclarativeParticipant( * @param identifier */ export function getRemoteParticipantKey( - identifier: - | CommunicationUserKind - | PhoneNumberKind - | CallingApplicationKind - | MicrosoftTeamsUserKind - | UnknownIdentifierKind + identifier: CommunicationUserKind | PhoneNumberKind | MicrosoftTeamsUserKind | UnknownIdentifierKind ): string { - let id = identifier.id; + let id = ''; switch (identifier.kind) { case 'communicationUser': { id = identifier.communicationUserId; @@ -78,10 +72,6 @@ export function getRemoteParticipantKey( id = identifier.phoneNumber; break; } - case 'callingApplication': { - id = identifier.callingApplicationId; - break; - } case 'microsoftTeamsUser': { id = identifier.microsoftTeamsUserId; break; @@ -107,7 +97,7 @@ export function convertSdkCallToDeclarativeCall(call: SdkCall): DeclarativeCall state: call.state, callEndReason: call.callEndReason, direction: call.direction, - isMicrophoneMuted: call.isMicrophoneMuted, + isMuted: call.isMuted, isScreenSharingOn: call.isScreenSharingOn, localVideoStreams: call.localVideoStreams.map(convertSdkLocalStreamToDeclarativeLocalStream), remoteParticipants: declarativeRemoteParticipants, diff --git a/packages/acs-calling-declarative/src/DeviceManagerDeclarative.test.ts b/packages/acs-calling-declarative/src/DeviceManagerDeclarative.test.ts index c7edce7e840..3baaf48da85 100644 --- a/packages/acs-calling-declarative/src/DeviceManagerDeclarative.test.ts +++ b/packages/acs-calling-declarative/src/DeviceManagerDeclarative.test.ts @@ -3,7 +3,6 @@ import { AudioDeviceInfo, AudioDeviceType, - CameraFacing, CollectionUpdatedEvent, DeviceAccess, DeviceManager, @@ -39,7 +38,6 @@ class MockDeviceManager implements DeviceManager { { name: 'camera', id: '3', - cameraFacing: 'Front' as CameraFacing, deviceType: 'UsbCamera' as VideoDeviceType } ]); diff --git a/packages/communication-ui/package.json b/packages/communication-ui/package.json index 2e6719fe3ad..eb21cf372f7 100644 --- a/packages/communication-ui/package.json +++ b/packages/communication-ui/package.json @@ -28,9 +28,10 @@ }, "dependencies": { "@azure/communication-administration": "1.0.0-beta.3", - "@azure/communication-calling": "1.0.0-beta.2", + "@azure/communication-calling": "1.0.1-beta.1", "@azure/communication-chat": "1.0.0-beta.4", - "@azure/communication-common": "1.0.0-beta.3", + "@azure/communication-common": "1.0.0", + "@azure/communication-common-beta3": "npm:@azure/communication-common@1.0.0-beta.3", "@azure/communication-signaling": "1.0.0-beta.1", "@azure/communication-signaling-2": "npm:@azure/communication-signaling@1.0.0-beta.2", "@azure/core-http": "^1.2.3", diff --git a/packages/communication-ui/src/composites/GroupCall/GroupCall.tsx b/packages/communication-ui/src/composites/GroupCall/GroupCall.tsx index c823ea1e052..a34df9e3c9f 100644 --- a/packages/communication-ui/src/composites/GroupCall/GroupCall.tsx +++ b/packages/communication-ui/src/composites/GroupCall/GroupCall.tsx @@ -48,8 +48,13 @@ export default (props: GroupCallCompositeProps): JSX.Element => { return ( - - + + {(() => { switch (page) { diff --git a/packages/communication-ui/src/composites/GroupCall/consumers/MapToGroupCallProps.ts b/packages/communication-ui/src/composites/GroupCall/consumers/MapToGroupCallProps.ts index 53d0ab6a509..ee68c13d4a2 100644 --- a/packages/communication-ui/src/composites/GroupCall/consumers/MapToGroupCallProps.ts +++ b/packages/communication-ui/src/composites/GroupCall/consumers/MapToGroupCallProps.ts @@ -1,6 +1,6 @@ // © Microsoft Corporation. All rights reserved. -import { CallState, HangupCallOptions } from '@azure/communication-calling'; +import { CallState, HangUpOptions } from '@azure/communication-calling'; import { useCallAgent } from '../../../hooks'; import { useGroupCall } from '../../../hooks/useGroupCall'; import { useCallContext, useCallingContext } from '../../../providers'; @@ -9,7 +9,7 @@ export type GroupCallContainerProps = { isCallInitialized: boolean; callState: CallState; isLocalScreenSharingOn: boolean; - leaveCall: (hangupCallOptions: HangupCallOptions) => Promise; + leaveCall: (hangupCallOptions: HangUpOptions) => Promise; }; export const MapToGroupCallProps = (): GroupCallContainerProps => { @@ -22,7 +22,7 @@ export const MapToGroupCallProps = (): GroupCallContainerProps => { isCallInitialized: !!(callAgent && deviceManager), callState: call?.state ?? 'None', isLocalScreenSharingOn: localScreenShareActive, - leaveCall: async (hangupCallOptions: HangupCallOptions) => { + leaveCall: async (hangupCallOptions: HangUpOptions) => { await leave(hangupCallOptions); } }; diff --git a/packages/communication-ui/src/composites/GroupCall/consumers/MapToMediaControlsProps.ts b/packages/communication-ui/src/composites/GroupCall/consumers/MapToMediaControlsProps.ts index 297abe17c8e..f5e9cf534e3 100644 --- a/packages/communication-ui/src/composites/GroupCall/consumers/MapToMediaControlsProps.ts +++ b/packages/communication-ui/src/composites/GroupCall/consumers/MapToMediaControlsProps.ts @@ -1,6 +1,6 @@ // © Microsoft Corporation. All rights reserved. -import { HangupCallOptions, PermissionState as DevicePermissionState } from '@azure/communication-calling'; +import { HangUpOptions } from '@azure/communication-calling'; import { useCallContext, useCallingContext } from '../../../providers'; import useSubscribeToDevicePermission from '../../../hooks/useSubscribeToDevicePermission'; import useLocalVideo from '../../../hooks/useLocalVideo'; @@ -11,6 +11,7 @@ import { isMobileSession } from '../../../utils'; import { useGroupCall } from '../../../hooks'; import { CommunicationUiErrorCode, CommunicationUiError } from '../../../types/CommunicationUiError'; import { useCallback } from 'react'; +import { DevicePermissionState } from '../../../types/DevicePermission'; export type MediaControlsContainerProps = { /** Determines icon for mic toggle button. */ @@ -48,7 +49,7 @@ export type MediaControlsContainerProps = { /** Determines if screen share is supported by browser. */ isLocalScreenShareSupportedInBrowser(): boolean; /** Callback when leaving the call. */ - leaveCall: (hangupCallOptions: HangupCallOptions) => Promise; + leaveCall: (hangupCallOptions: HangUpOptions) => Promise; }; export const MapToMediaControlsProps = (): MediaControlsContainerProps => { @@ -110,7 +111,7 @@ export const MapToMediaControlsProps = (): MediaControlsContainerProps => { cameraPermission: videoDevicePermission, micPermission: audioDevicePermission, isLocalScreenShareSupportedInBrowser, - leaveCall: async (hangupCallOptions: HangupCallOptions): Promise => { + leaveCall: async (hangupCallOptions: HangUpOptions): Promise => { await leave(hangupCallOptions); } }; diff --git a/packages/communication-ui/src/composites/GroupCall/consumers/MapToMediaGalleryProps.ts b/packages/communication-ui/src/composites/GroupCall/consumers/MapToMediaGalleryProps.ts index 7915219d747..9fa1fa165ae 100644 --- a/packages/communication-ui/src/composites/GroupCall/consumers/MapToMediaGalleryProps.ts +++ b/packages/communication-ui/src/composites/GroupCall/consumers/MapToMediaGalleryProps.ts @@ -16,8 +16,8 @@ export type MediaGalleryContainerProps = { }; export const MapToMediaGalleryProps = (): MediaGalleryContainerProps => { - const { participants, displayName, screenShareStream, localVideoStream } = useCallContext(); - const { userId } = useCallingContext(); + const { participants, screenShareStream, localVideoStream } = useCallContext(); + const { userId, displayName } = useCallingContext(); const [remoteParticipants, setRemoteParticipants] = useState([]); const [localParticipant, setLocalParticipant] = useState({ userId, diff --git a/packages/communication-ui/src/composites/OneToOneCall/CallListener.tsx b/packages/communication-ui/src/composites/OneToOneCall/CallListener.tsx index 2df1454fde5..780e4aacda9 100644 --- a/packages/communication-ui/src/composites/OneToOneCall/CallListener.tsx +++ b/packages/communication-ui/src/composites/OneToOneCall/CallListener.tsx @@ -1,5 +1,5 @@ // © Microsoft Corporation. All rights reserved. -import { Call } from '@azure/communication-calling'; +import { IncomingCall } from '@azure/communication-calling'; import { Stack } from '@fluentui/react'; import React, { useEffect, useState } from 'react'; import { IncomingCallToast, IncomingCallToastProps } from './IncomingCallAlerts'; @@ -11,12 +11,12 @@ export type IncomingCallProps = { onIncomingCallRejected?: () => void; }; -const IncomingCallAlertACSWrapper = (props: IncomingCallToastProps & { call: Call }): JSX.Element => { +const IncomingCallAlertACSWrapper = (props: IncomingCallToastProps & { call: IncomingCall }): JSX.Element => { const { call } = props; const [callerName, setCallerName] = useState(undefined); useEffect(() => { - setCallerName(call.remoteParticipants[0]?.displayName); + setCallerName(call.callerInfo.displayName); }, [call]); return ; diff --git a/packages/communication-ui/src/composites/OneToOneCall/CallScreen.tsx b/packages/communication-ui/src/composites/OneToOneCall/CallScreen.tsx index d3ff8de7118..aa66d5f0789 100644 --- a/packages/communication-ui/src/composites/OneToOneCall/CallScreen.tsx +++ b/packages/communication-ui/src/composites/OneToOneCall/CallScreen.tsx @@ -1,7 +1,7 @@ // © Microsoft Corporation. All rights reserved. import { Label, Stack } from '@fluentui/react'; -import React from 'react'; +import React, { useEffect } from 'react'; import { activeContainerClassName, containerStyles, loadingStyle } from './styles/CallScreen.styles'; import MediaFullScreen from './MediaFullScreen'; import { connectFuncsToContext } from '../../consumers/ConnectContext'; @@ -17,14 +17,26 @@ export interface OneToOneCallProps extends CallContainerProps { } const CallScreenComponent = (props: OneToOneCallProps): JSX.Element => { - const { callState, isCallInitialized, screenShareStream, isLocalScreenSharingOn, endCallHandler } = props; + const { + callState, + isCallInitialized, + screenShareStream, + isLocalScreenSharingOn, + endCallHandler, + callFailedHandler + } = props; + + // In the OneToOne Sample, the handler is used to change the parent OneToOneCall's state. This causes an error: + // 'Cannot update a component (`OneToOneCall`) while rendering a different component (`CallScreenComponent`)'. Moved + // callFailedHandler inside a useEffect so it runs after the render to fix the error. + useEffect(() => { + if (!callState || callState === 'Disconnected') callFailedHandler(); + }, [callState, callFailedHandler]); if (!isCallInitialized || callState === 'None' || callState === 'Connecting' || callState === 'Ringing') { return ; } - if (!callState || callState === 'Disconnected') props.callFailedHandler(); - return ( { const [loading, setLoading] = useState(true); const fullScreenStreamMediaId = 'fullScreenStreamMediaId'; - const rendererViewRef: React.MutableRefObject = useRef(null); + const rendererViewRef: React.MutableRefObject = useRef(null); /** * Start stream after DOM has rendered @@ -27,7 +27,7 @@ export default (props: MediaFullScreenProps): JSX.Element => { (async () => { if (activeScreenShareStream && activeScreenShareStream.stream) { const stream: RemoteVideoStream = activeScreenShareStream.stream; - const renderer: Renderer = new Renderer(stream); + const renderer: VideoStreamRenderer = new VideoStreamRenderer(stream); rendererViewRef.current = await renderer.createView({ scalingMode: 'Fit' }); const container = document.getElementById(fullScreenStreamMediaId); diff --git a/packages/communication-ui/src/composites/OneToOneCall/OneToOneCall.tsx b/packages/communication-ui/src/composites/OneToOneCall/OneToOneCall.tsx index 16e342fcf71..6b1787cdd1e 100644 --- a/packages/communication-ui/src/composites/OneToOneCall/OneToOneCall.tsx +++ b/packages/communication-ui/src/composites/OneToOneCall/OneToOneCall.tsx @@ -59,8 +59,13 @@ export const OneToOneCall = (props: OneToOneCallCompositeProps): JSX.Element => return ( - - + + {(() => { switch (page) { case 'landing': { diff --git a/packages/communication-ui/src/composites/OneToOneCall/consumers/MapToMediaGallery1To1Props.ts b/packages/communication-ui/src/composites/OneToOneCall/consumers/MapToMediaGallery1To1Props.ts index a760a142679..38a804ecc3f 100644 --- a/packages/communication-ui/src/composites/OneToOneCall/consumers/MapToMediaGallery1To1Props.ts +++ b/packages/communication-ui/src/composites/OneToOneCall/consumers/MapToMediaGallery1To1Props.ts @@ -1,8 +1,8 @@ // © Microsoft Corporation. All rights reserved. -import { LocalVideoStream } from '@azure/communication-calling'; -import { useEffect, useState } from 'react'; -import { useCallContext } from '../../../providers'; +import { LocalVideoStream, RemoteParticipant } from '@azure/communication-calling'; +import { useEffect, useRef, useState } from 'react'; +import { useCallContext, useCallingContext } from '../../../providers'; import { GalleryParticipant } from '../../../types/GalleryParticipant'; import { convertSdkRemoteParticipantToGalleryParticipant } from '../../../utils/TypeConverter'; @@ -15,14 +15,46 @@ export type MediaGallery1To1ContainerProps = { localVideoStream: LocalVideoStream | undefined; }; +// On video calls the displayNameChanged event happens very late and ends up being after the remote participant is +// already rendered with the userId. We need to listen for this event and trigger a re-render after displayNameChanged. +class DisplayNameChangedSubscriber { + private _participant: RemoteParticipant; + private _setRemoteParticipant: (remoteParticipant: GalleryParticipant) => void; + + constructor(participant: RemoteParticipant, setRemoteParticipant: (remoteParticipant: GalleryParticipant) => void) { + this._participant = participant; + this._setRemoteParticipant = setRemoteParticipant; + this._participant.on('displayNameChanged', this.onDisplayNameChanged); + } + + private onDisplayNameChanged = (): void => { + this._setRemoteParticipant(convertSdkRemoteParticipantToGalleryParticipant(this._participant)); + }; + + public unsubscribe = (): void => { + this._participant.off('displayNameChanged', this.onDisplayNameChanged); + }; +} + export const MapToMediaGallery1To1Props = (): MediaGallery1To1ContainerProps => { - const { call, displayName, localVideoStream } = useCallContext(); + const { call, localVideoStream } = useCallContext(); + const { displayName } = useCallingContext(); const [remoteParticipant, setRemoteParticipant] = useState(); + const displayNameChangedSubscriber = useRef(undefined); useEffect(() => { if (call && call.remoteParticipants.length > 0) { setRemoteParticipant(convertSdkRemoteParticipantToGalleryParticipant(call.remoteParticipants[0])); + displayNameChangedSubscriber.current = new DisplayNameChangedSubscriber( + call.remoteParticipants[0], + setRemoteParticipant + ); } + return () => { + if (displayNameChangedSubscriber.current) { + displayNameChangedSubscriber.current.unsubscribe(); + } + }; }, [call]); return { diff --git a/packages/communication-ui/src/composites/common/consumers/MapToCallControlBarProps.ts b/packages/communication-ui/src/composites/common/consumers/MapToCallControlBarProps.ts index 9a821b20746..2a3fe55d806 100644 --- a/packages/communication-ui/src/composites/common/consumers/MapToCallControlBarProps.ts +++ b/packages/communication-ui/src/composites/common/consumers/MapToCallControlBarProps.ts @@ -1,8 +1,8 @@ // © Microsoft Corporation. All rights reserved. -import { HangupCallOptions, PermissionState as DevicePermissionState } from '@azure/communication-calling'; +import { HangUpOptions } from '@azure/communication-calling'; import { useCallContext, useCallingContext } from '../../../providers'; -import { CommunicationUiErrorCode, CommunicationUiError } from '../../../types'; +import { CommunicationUiErrorCode, CommunicationUiError, DevicePermissionState } from '../../../types'; import { useSubscribeToDevicePermission, useLocalVideo, @@ -48,7 +48,7 @@ export type CallControlBarContainerProps = { /** Determines mic permission. */ micPermission: DevicePermissionState; /** Callback when leaving the call. */ - leaveCall: (hangupCallOptions: HangupCallOptions) => Promise; + leaveCall: (hangupCallOptions: HangUpOptions) => Promise; }; export const MapToCallControlBarProps = (): CallControlBarContainerProps => { @@ -101,7 +101,7 @@ export const MapToCallControlBarProps = (): CallControlBarContainerProps => { toggleScreenShare, cameraPermission: videoDevicePermission, micPermission: audioDevicePermission, - leaveCall: async (hangupCallOptions: HangupCallOptions): Promise => { + leaveCall: async (hangupCallOptions: HangUpOptions): Promise => { await leave(hangupCallOptions); } }; diff --git a/packages/communication-ui/src/consumers/MapToCallConfigurationProps.ts b/packages/communication-ui/src/consumers/MapToCallConfigurationProps.ts index 61254c36e3a..df7eac78d55 100644 --- a/packages/communication-ui/src/consumers/MapToCallConfigurationProps.ts +++ b/packages/communication-ui/src/consumers/MapToCallConfigurationProps.ts @@ -7,25 +7,18 @@ import { useCallAgent } from '../hooks'; export type SetupContainerProps = { isCallInitialized: boolean; displayName: string; - updateDisplayName: (displayName: string) => void; joinCall: (groupId: string) => void; }; export const MapToCallConfigurationProps = (): SetupContainerProps => { - const { callAgent, deviceManager } = useCallingContext(); - const { call, displayName, setDisplayName } = useCallContext(); + const { callAgent, deviceManager, displayName } = useCallingContext(); + const { call } = useCallContext(); const { join } = useGroupCall(); useCallAgent(); - const updateDisplayName = (displayName: string): void => { - callAgent?.updateDisplayName(displayName); - setDisplayName(displayName); - }; - return { isCallInitialized: !!(callAgent && deviceManager), displayName, - updateDisplayName, joinCall: (groupId: string) => { !call && join({ groupId: groupId }); } diff --git a/packages/communication-ui/src/consumers/MapToCallProps.ts b/packages/communication-ui/src/consumers/MapToCallProps.ts index f8bdd578aa0..0fdbb4d4f55 100644 --- a/packages/communication-ui/src/consumers/MapToCallProps.ts +++ b/packages/communication-ui/src/consumers/MapToCallProps.ts @@ -1,6 +1,6 @@ // © Microsoft Corporation. All rights reserved. -import { CallState, HangupCallOptions } from '@azure/communication-calling'; +import { CallState, HangUpOptions } from '@azure/communication-calling'; import { useCallContext, useCallingContext } from '../providers'; import { ParticipantStream } from '../types/ParticipantStream'; import { useOutgoingCall } from '../hooks'; @@ -10,7 +10,7 @@ export type CallContainerProps = { callState: CallState; screenShareStream: ParticipantStream | undefined; isLocalScreenSharingOn: boolean; - leaveCall: (hangupCallOptions: HangupCallOptions) => Promise; + leaveCall: (hangupCallOptions: HangUpOptions) => Promise; }; export const MapToOneToOneCallProps = (): CallContainerProps => { diff --git a/packages/communication-ui/src/consumers/MapToLocalDeviceSettingsProps.ts b/packages/communication-ui/src/consumers/MapToLocalDeviceSettingsProps.ts index 5aee81a4d6c..6f8bdb135f8 100644 --- a/packages/communication-ui/src/consumers/MapToLocalDeviceSettingsProps.ts +++ b/packages/communication-ui/src/consumers/MapToLocalDeviceSettingsProps.ts @@ -64,7 +64,7 @@ export const MapToLocalDeviceSettingsProps = (): LocalDeviceSettingsContainerPro const updateAudioDeviceInfo = (source: AudioDeviceInfo): void => { if (source) { setAudioDeviceInfo(source); - deviceManager?.setMicrophone(source); + deviceManager?.selectMicrophone(source); } }; diff --git a/packages/communication-ui/src/hooks/useCallAgent.test.ts b/packages/communication-ui/src/hooks/useCallAgent.test.ts index 6e05d79dbb2..67c0e313d36 100644 --- a/packages/communication-ui/src/hooks/useCallAgent.test.ts +++ b/packages/communication-ui/src/hooks/useCallAgent.test.ts @@ -138,21 +138,11 @@ describe('useCallAgent tests', () => { expect(callsUpdatedEvent).toBeDefined(); }); - test('after callsUpdated listener is called, useCallAgent hook should reject the addedcall if it is incoming', async () => { - addedCall.isIncoming = true; - - renderHook(() => useCallAgent()); - await events.callsUpdated({ added: [addedCall], removed: [] }); - - expect(rejectExecutedCallback).toHaveBeenCalled(); - expect(setCallCallback).not.toHaveBeenCalled(); - }); - - test('after callsUpdated listener is called, if the addedCall is defined and not incoming, addedCall should subscibe for callStateChanged and remoteParticipantsUpdated', async () => { + test('after callsUpdated listener is called, if the addedCall is defined and not incoming, addedCall should subscibe for stateChanged and remoteParticipantsUpdated', async () => { renderHook(() => useCallAgent()); await events.callsUpdated({ added: [addedCall], removed: [] }); - expect(addedCallEvents.callStateChanged).toBeDefined(); + expect(addedCallEvents.stateChanged).toBeDefined(); expect(addedCallEvents.remoteParticipantsUpdated).toBeDefined(); }); @@ -163,13 +153,13 @@ describe('useCallAgent tests', () => { expect(setCallCallback).toHaveBeenCalled(); }); - test('if callStateChanged listener of the added call is called, setCallState should have been called', async () => { + test('if stateChanged listener of the added call is called, setCallState should have been called', async () => { expect(setCallStateCallback).not.toHaveBeenCalled(); renderHook(() => useCallAgent()); await events.callsUpdated({ added: [addedCall], removed: [] }); - const callStateChangedCallback = addedCallEvents.callStateChanged as PropertyChangedEvent; + const callStateChangedCallback = addedCallEvents.stateChanged as PropertyChangedEvent; callStateChangedCallback(); expect(setCallStateCallback).toHaveBeenCalled(); @@ -202,17 +192,17 @@ describe('useCallAgent tests', () => { expect(setParticipantsCallback).toHaveBeenCalled(); }); - test('if added call has remoteParticipants, then the remoteParticipants should subsribe to participantStateChanged, isSpeakingChanged, videoStreamsUpdated', async () => { + test('if added call has remoteParticipants, then the remoteParticipants should subsribe to stateChanged, isSpeakingChanged, videoStreamsUpdated', async () => { renderHook(() => useCallAgent()); await events.callsUpdated({ added: [addedCall], removed: [] }); - expect(remoteParticipantEvents.participantStateChanged).toBeDefined(); + expect(remoteParticipantEvents.stateChanged).toBeDefined(); expect(remoteParticipantEvents.isSpeakingChanged).toBeDefined(); expect(remoteParticipantEvents.videoStreamsUpdated).toBeDefined(); }); test('if addedRemoteVideoStream is type video, it should not subscribe to any events', async () => { - addedRemoteVideoStream.type = 'Video'; + addedRemoteVideoStream.mediaStreamType = 'Video'; renderHook(() => useCallAgent()); await events.callsUpdated({ added: [addedCall], removed: [] }); @@ -224,8 +214,8 @@ describe('useCallAgent tests', () => { expect(addedRemoteVideoStreamEvents).toStrictEqual({}); }); - test('if addedRemoteVideoStream is type ScreenSharing, it should not subscribe to availabilityChanged event', async () => { - addedRemoteVideoStream.type = 'ScreenSharing'; + test('if addedRemoteVideoStream is type ScreenSharing, it should not subscribe to isAvailableChanged event', async () => { + addedRemoteVideoStream.mediaStreamType = 'ScreenSharing'; renderHook(() => useCallAgent()); await events.callsUpdated({ added: [addedCall], removed: [] }); @@ -234,11 +224,11 @@ describe('useCallAgent tests', () => { >; videoStreamsUpdatedCallback({ added: [addedRemoteVideoStream], removed: [] }); - expect(addedRemoteVideoStreamEvents.availabilityChanged).toBeDefined(); + expect(addedRemoteVideoStreamEvents.isAvailableChanged).toBeDefined(); }); test('if addedRemoteVideoStream is type ScreenSharing and available, setScreenShareStream should have been called', async () => { - addedRemoteVideoStream.type = 'ScreenSharing'; + addedRemoteVideoStream.mediaStreamType = 'ScreenSharing'; addedRemoteVideoStream.isAvailable = true; renderHook(() => useCallAgent()); await events.callsUpdated({ added: [addedCall], removed: [] }); @@ -252,7 +242,7 @@ describe('useCallAgent tests', () => { }); test('if addedRemoteVideoStream is type ScreenSharing and not available, setScreenShareStream should not have been called', async () => { - addedRemoteVideoStream.type = 'ScreenSharing'; + addedRemoteVideoStream.mediaStreamType = 'ScreenSharing'; addedRemoteVideoStream.isAvailable = false; renderHook(() => useCallAgent()); await events.callsUpdated({ added: [addedCall], removed: [] }); @@ -265,8 +255,8 @@ describe('useCallAgent tests', () => { expect(setScreenShareStreamCallback).not.toHaveBeenCalled(); }); - test('if availabilityChanged of addedRemoteVideoStream and addedRemoteVideoStream is not available, setScreenShareStream should have been called with undefined', async () => { - addedRemoteVideoStream.type = 'ScreenSharing'; + test('if isAvailableChanged of addedRemoteVideoStream and addedRemoteVideoStream is not available, setScreenShareStream should have been called with undefined', async () => { + addedRemoteVideoStream.mediaStreamType = 'ScreenSharing'; addedRemoteVideoStream.isAvailable = false; renderHook(() => useCallAgent()); await events.callsUpdated({ added: [addedCall], removed: [] }); @@ -276,13 +266,13 @@ describe('useCallAgent tests', () => { >; videoStreamsUpdatedCallback({ added: [addedRemoteVideoStream], removed: [] }); - addedRemoteVideoStreamEvents.availabilityChanged(); + addedRemoteVideoStreamEvents.isAvailableChanged(); expect(setScreenShareStreamCallback).toHaveBeenCalledWith(undefined); }); - test('if availabilityChanged of addedRemoteVideoStream is called and addedRemoteVideoStream is available, setScreenShareStream should have been called', async () => { - addedRemoteVideoStream.type = 'ScreenSharing'; + test('if isAvailableChanged of addedRemoteVideoStream is called and addedRemoteVideoStream is available, setScreenShareStream should have been called', async () => { + addedRemoteVideoStream.mediaStreamType = 'ScreenSharing'; addedRemoteVideoStream.isAvailable = false; renderHook(() => useCallAgent()); await events.callsUpdated({ added: [addedCall], removed: [] }); @@ -295,7 +285,7 @@ describe('useCallAgent tests', () => { expect(setScreenShareStreamCallback).not.toHaveBeenCalled(); - addedRemoteVideoStreamEvents.availabilityChanged(); + addedRemoteVideoStreamEvents.isAvailableChanged(); expect(setScreenShareStreamCallback).toHaveBeenCalled(); }); diff --git a/packages/communication-ui/src/hooks/useCallAgent.ts b/packages/communication-ui/src/hooks/useCallAgent.ts index f5c5c1b6338..198f4b6d746 100644 --- a/packages/communication-ui/src/hooks/useCallAgent.ts +++ b/packages/communication-ui/src/hooks/useCallAgent.ts @@ -3,7 +3,7 @@ import { Call, CallState, RemoteParticipant } from '@azure/communication-calling'; import { useCallingContext, useCallContext } from '../providers'; import { ParticipantStream } from '../types/ParticipantStream'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { getACSId } from '../utils'; export type UseCallAgentType = { @@ -16,7 +16,7 @@ export type UseCallAgentType = { screenShareStream: ParticipantStream | undefined; }; -export default (): void => { +export default (): boolean => { const { callAgent } = useCallingContext(); const { call, @@ -28,10 +28,12 @@ export default (): void => { setLocalScreenShare } = useCallContext(); + const [subscribed, setSubscribed] = useState(false); + useEffect(() => { const subscribeToParticipant = (participant: RemoteParticipant, call: Call): void => { const userId = getACSId(participant.identifier); - participant.on('participantStateChanged', () => { + participant.on('stateChanged', () => { console.log('participant stateChanged', userId, participant.state); setParticipants([...call.remoteParticipants.values()]); }); @@ -54,10 +56,10 @@ export default (): void => { participant.on('videoStreamsUpdated', (e): void => { e.added.forEach((addedStream) => { - if (addedStream.type === 'Video') { + if (addedStream.mediaStreamType === 'Video') { return; } - addedStream.on('availabilityChanged', () => { + addedStream.on('isAvailableChanged', () => { if (addedStream.isAvailable) { setScreenShareStream({ stream: addedStream, user: participant }); } else { @@ -80,13 +82,8 @@ export default (): void => { const onCallsUpdated = (e: { added: Call[]; removed: Call[] }): void => { e.added.forEach((addedCall) => { - if (call && addedCall.isIncoming) { - addedCall.reject(); - return; - } - setCall(addedCall); - addedCall.on('callStateChanged', (): void => { + addedCall.on('stateChanged', (): void => { setCallState(addedCall.state); }); addedCall.on('remoteParticipantsUpdated', (ev): void => { @@ -119,9 +116,13 @@ export default (): void => { }); }; - callAgent?.on('callsUpdated', onCallsUpdated); + if (callAgent) { + callAgent.on('callsUpdated', onCallsUpdated); + setSubscribed(true); + } return () => { callAgent?.off('callsUpdated', onCallsUpdated); + setSubscribed(false); }; }, [ call, @@ -133,4 +134,8 @@ export default (): void => { setLocalScreenShare, screenShareStream ]); + + // Need refactor: because callAgent is created asynchronously and this useEffect is run asynchronously, any usages + // of CallAgent like join/start inbetween it being created and subscribed will not have the event be picked up. + return subscribed; }; diff --git a/packages/communication-ui/src/hooks/useGroupCall.ts b/packages/communication-ui/src/hooks/useGroupCall.ts index ccf81ecd1b8..92a083212d7 100644 --- a/packages/communication-ui/src/hooks/useGroupCall.ts +++ b/packages/communication-ui/src/hooks/useGroupCall.ts @@ -1,13 +1,13 @@ // © Microsoft Corporation. All rights reserved. -import { AudioOptions, Call, GroupCallContext, HangupCallOptions, JoinCallOptions } from '@azure/communication-calling'; +import { AudioOptions, Call, GroupLocator, HangUpOptions, JoinCallOptions } from '@azure/communication-calling'; import { useCallback } from 'react'; import { CommunicationUiErrorCode, CommunicationUiError } from '../types/CommunicationUiError'; import { useCallingContext, useCallContext } from '../providers'; export type UseGroupCallType = { - leave: (hangupCallOptions: HangupCallOptions) => Promise; - join: (context: GroupCallContext, joinCallOptions?: JoinCallOptions) => Call; + leave: (hangupCallOptions: HangUpOptions) => Promise; + join: (context: GroupLocator, joinCallOptions?: JoinCallOptions) => Call; }; export const useGroupCall = (): UseGroupCallType => { @@ -15,7 +15,7 @@ export const useGroupCall = (): UseGroupCallType => { const { call, localVideoStream, isMicrophoneEnabled } = useCallContext(); const join = useCallback( - (context: GroupCallContext, joinCallOptions?: JoinCallOptions): Call => { + (context: GroupLocator, joinCallOptions?: JoinCallOptions): Call => { if (!callAgent) { throw new CommunicationUiError({ message: 'CallAgent is undefined', @@ -43,7 +43,7 @@ export const useGroupCall = (): UseGroupCallType => { ); const leave = useCallback( - async (hangupCallOptions: HangupCallOptions): Promise => { + async (hangupCallOptions: HangUpOptions): Promise => { if (!call) { throw new CommunicationUiError({ message: 'Call is invalid', diff --git a/packages/communication-ui/src/hooks/useIncomingCall.ts b/packages/communication-ui/src/hooks/useIncomingCall.ts index a32584c21fb..5dd4fe8d56d 100644 --- a/packages/communication-ui/src/hooks/useIncomingCall.ts +++ b/packages/communication-ui/src/hooks/useIncomingCall.ts @@ -1,20 +1,20 @@ // © Microsoft Corporation. All rights reserved. -import { AcceptCallOptions, Call } from '@azure/communication-calling'; +import { AcceptCallOptions, IncomingCall } from '@azure/communication-calling'; import { useCallContext, useIncomingCallsContext } from '../providers'; export type UseIncomingCallType = { - accept: (incomingCall: Call, acceptCallOptions?: AcceptCallOptions) => Promise; - reject: (incomingCall: Call) => Promise; - incomingCalls: Call[]; + accept: (incomingCall: IncomingCall, acceptCallOptions?: AcceptCallOptions) => Promise; + reject: (incomingCall: IncomingCall) => Promise; + incomingCalls: IncomingCall[]; }; export const useIncomingCall = (): UseIncomingCallType => { - const { incomingCalls } = useIncomingCallsContext(); + const { incomingCalls, setIncomingCalls } = useIncomingCallsContext(); const { setCall, localVideoStream, setScreenShareStream, screenShareStream } = useCallContext(); /** Accept an incoming calls and set it as the active call. */ - const accept = async (incomingCall: Call, acceptCallOptions?: AcceptCallOptions): Promise => { + const accept = async (incomingCall: IncomingCall, acceptCallOptions?: AcceptCallOptions): Promise => { if (!incomingCall) { throw new Error('incomingCall is null or undefined'); } @@ -23,16 +23,25 @@ export const useIncomingCall = (): UseIncomingCallType => { localVideoStreams: localVideoStream ? [localVideoStream] : undefined }; - await incomingCall.accept({ videoOptions }); - setCall(incomingCall); + const call = await incomingCall.accept({ videoOptions }); + setCall(call); + + // Remove the accepted incomingCall from incomingCalls + const incomingCallsFiltered: IncomingCall[] = []; + for (const incomingCallCandidate of incomingCalls) { + if (incomingCallCandidate !== incomingCall) { + incomingCallsFiltered.push(incomingCallCandidate); + } + } + setIncomingCalls(incomingCallsFiltered); // Listen to Remote Participant screen share stream // Should we move this logic to CallProvider ? - incomingCall.remoteParticipants.forEach((participant) => { + call.remoteParticipants.forEach((participant) => { participant.on('videoStreamsUpdated', (e) => { e.added.forEach((addedStream) => { - if (addedStream.type === 'Video') return; - addedStream.on('availabilityChanged', () => { + if (addedStream.mediaStreamType === 'Video') return; + addedStream.on('isAvailableChanged', () => { if (addedStream.isAvailable) { setScreenShareStream({ stream: addedStream, user: participant }); } else { @@ -54,7 +63,7 @@ export const useIncomingCall = (): UseIncomingCallType => { }; /** Reject an incoming call and remove it from `incomingCalls`. */ - const reject = async (incomingCall: Call): Promise => { + const reject = async (incomingCall: IncomingCall): Promise => { if (!incomingCall) { throw new Error('incomingCall is null or undefined'); } diff --git a/packages/communication-ui/src/hooks/useLocalVideo.test.ts b/packages/communication-ui/src/hooks/useLocalVideo.test.ts index b42cb36c845..9509adbd0db 100644 --- a/packages/communication-ui/src/hooks/useLocalVideo.test.ts +++ b/packages/communication-ui/src/hooks/useLocalVideo.test.ts @@ -14,7 +14,11 @@ type MockCallContextType = { setLocalVideoOn: jest.Mock; }; -const mockVideoDeviceInfo = { name: 'Microsoft Camera Front', id: 'camera:54321' } as VideoDeviceInfo; +const mockVideoDeviceInfo = { + name: 'Microsoft Camera Front', + id: 'camera:54321', + deviceType: 'Unknown' +} as VideoDeviceInfo; let startVideo = jest.fn(); let stopVideo = jest.fn(); let setLocalVideoOn = jest.fn(); @@ -31,6 +35,18 @@ jest.mock('../providers', () => { }; }); +jest.mock('@azure/communication-calling', () => { + return { + LocalVideoStream: jest.fn().mockImplementation((videoDeviceInfo: VideoDeviceInfo) => { + return { + source: videoDeviceInfo, + mediaStreamType: 'Video', + switchSource: jest.fn() + }; + }) + }; +}); + describe('useLocalVideo tests', () => { beforeEach(() => { startVideo = jest.fn(); @@ -54,7 +70,7 @@ describe('useLocalVideo tests', () => { test('Upon calling startLocalVideo `call.startCall` function should be triggered when localVideoStream is defined.', async () => { const { result } = renderHook(() => useLocalVideo()); // TODO: fix typescript types - await result.current.startLocalVideo(mockCallContext().localVideoStream as any); + await result.current.startLocalVideo(mockCallContext().localVideoStream); expect(startVideo).toBeCalled(); expect(setLocalVideoOn).toBeCalledWith(true); }); @@ -70,7 +86,7 @@ describe('useLocalVideo tests', () => { const { result } = renderHook(() => useLocalVideo()); // TODO: fix typescript types - await result.current.startLocalVideo(mockCallContext().localVideoStream as any).catch((e: Error) => { + await result.current.startLocalVideo(mockCallContext().localVideoStream).catch((e: Error) => { expect(e.message).toEqual('Failed to start local video: local video renderer is busy'); }); expect(startVideo).not.toBeCalled(); @@ -87,7 +103,7 @@ describe('useLocalVideo tests', () => { }; const { result } = renderHook(() => useLocalVideo()); // TODO: fix typescript types - await result.current.startLocalVideo(mockCallContext().localVideoStream as any); + await result.current.startLocalVideo(mockCallContext().localVideoStream); expect(startVideo).toBeCalledTimes(1); expect(setLocalVideoOn).toBeCalledWith(true); }); diff --git a/packages/communication-ui/src/hooks/useLocalVideoStreamRenderer.test.ts b/packages/communication-ui/src/hooks/useLocalVideoStreamRenderer.test.ts index ab095e39f19..474fa869132 100644 --- a/packages/communication-ui/src/hooks/useLocalVideoStreamRenderer.test.ts +++ b/packages/communication-ui/src/hooks/useLocalVideoStreamRenderer.test.ts @@ -11,7 +11,7 @@ let mockCallContext: () => MockCallContextType; jest.mock('@azure/communication-calling', () => { return { - Renderer: jest.fn().mockImplementation(() => { + VideoStreamRenderer: jest.fn().mockImplementation(() => { return { createView: jest.fn().mockImplementation(() => { return { @@ -45,8 +45,7 @@ jest.mock('../providers/ErrorProvider', () => { const videoDeviceInfoStub: VideoDeviceInfo = { id: '1', name: 'camera', - deviceType: 'Unknown', - cameraFacing: 'Front' + deviceType: 'Unknown' }; describe('useLocalVideoStreamRenderer tests', () => { diff --git a/packages/communication-ui/src/hooks/useLocalVideoStreamRenderer.ts b/packages/communication-ui/src/hooks/useLocalVideoStreamRenderer.ts index 29b67f8bae1..1eb2a9e9dec 100644 --- a/packages/communication-ui/src/hooks/useLocalVideoStreamRenderer.ts +++ b/packages/communication-ui/src/hooks/useLocalVideoStreamRenderer.ts @@ -1,5 +1,10 @@ // © Microsoft Corporation. All rights reserved. -import { LocalVideoStream, Renderer, RendererOptions, RendererView } from '@azure/communication-calling'; +import { + LocalVideoStream, + VideoStreamRenderer, + CreateViewOptions, + VideoStreamRendererView +} from '@azure/communication-calling'; import { useCallContext } from '../providers'; import { useEffect, useRef, useState } from 'react'; import { CommunicationUiErrorCode, CommunicationUiError } from '../types/CommunicationUiError'; @@ -15,14 +20,14 @@ export type UseLocalVideoStreamType = { // It also has an event handler to say when the stream is available or not since it can be difficult to tell from the returned HTMLElement. export default ( stream: LocalVideoStream | undefined, - rendererOptions: RendererOptions | undefined + rendererOptions: CreateViewOptions | undefined ): UseLocalVideoStreamType => { const onErrorCallback = useTriggerOnErrorCallback(); const { setLocalVideoRendererBusy } = useCallContext(); const [render, setRender] = useState(null); const [isAvailable, setIsAvailable] = useState(false); - const [options] = useState(rendererOptions); - const rendererViewRef: React.MutableRefObject = useRef(null); + const [options] = useState(rendererOptions); + const rendererViewRef: React.MutableRefObject = useRef(null); const cleanUp = (): void => { if (rendererViewRef.current && rendererViewRef.current.dispose) { @@ -34,7 +39,7 @@ export default ( useEffect(() => { const renderStream = async (stream: LocalVideoStream): Promise => { setLocalVideoRendererBusy(true); - const renderer = new Renderer(stream); + const renderer = new VideoStreamRenderer(stream); try { rendererViewRef.current = await renderer.createView(options); } catch (error) { diff --git a/packages/communication-ui/src/hooks/useMicrophone.test.ts b/packages/communication-ui/src/hooks/useMicrophone.test.ts index c04fdb72ba3..87f8a3178dc 100644 --- a/packages/communication-ui/src/hooks/useMicrophone.test.ts +++ b/packages/communication-ui/src/hooks/useMicrophone.test.ts @@ -1,13 +1,14 @@ // © Microsoft Corporation. All rights reserved. import { renderHook } from '@testing-library/react-hooks'; import { useMicrophone } from './useMicrophone'; -import { CallAgent, PermissionState } from '@azure/communication-calling'; +import { CallAgent } from '@azure/communication-calling'; import { defaultMockCallProps, mockCallAgent } from '../mocks'; import { CommunicationUiError } from '../types/CommunicationUiError'; +import { DevicePermissionState } from '../types/DevicePermission'; type MockCallingContextType = { callAgent: CallAgent; - audioDevicePermission: PermissionState; + audioDevicePermission: DevicePermissionState; }; type MockCallContextType = { @@ -19,7 +20,7 @@ let muteExecuted = jest.fn(); let unmuteExecuted = jest.fn(); let setIsMicrophoneEnabledMock = jest.fn(); let microphoneMutedInitialState = false; -let microphonePermissionInitialState: PermissionState = 'Granted'; +let microphonePermissionInitialState: DevicePermissionState = 'Granted'; let mockCallingContext: () => MockCallingContextType; let mockCallContext: () => MockCallContextType; @@ -103,7 +104,7 @@ describe('useMicrophone tests', () => { unmuteExecutedCallback: unmuteExecuted, isMicrophoneMuted: microphoneMutedInitialState }); - callAgent.calls[0].isMicrophoneMuted = false; + callAgent.calls[0].isMuted = false; mockCallingContext = (): MockCallingContextType => { return { // TODO: fix typescript types @@ -153,7 +154,7 @@ describe('useMicrophone tests', () => { unmuteExecutedCallback: unmuteExecuted, isMicrophoneMuted: microphoneMutedInitialState }); - callAgent.calls[0].isMicrophoneMuted = true; + callAgent.calls[0].isMuted = true; mockCallingContext = (): MockCallingContextType => { return { // TODO: fix typescript types diff --git a/packages/communication-ui/src/hooks/useMicrophone.ts b/packages/communication-ui/src/hooks/useMicrophone.ts index a294f6a5ab2..4a506c9bbff 100644 --- a/packages/communication-ui/src/hooks/useMicrophone.ts +++ b/packages/communication-ui/src/hooks/useMicrophone.ts @@ -24,7 +24,7 @@ export const useMicrophone = (): UseMicrophoneType => { } try { - if (call?.isMicrophoneMuted) { + if (call?.isMuted) { await call.unmute(); } setIsMicrophoneEnabled(true); @@ -39,7 +39,7 @@ export const useMicrophone = (): UseMicrophoneType => { const mute = useCallback(async (): Promise => { try { - if (!call?.isMicrophoneMuted) { + if (!call?.isMuted) { await call?.mute(); } setIsMicrophoneEnabled(false); diff --git a/packages/communication-ui/src/hooks/useOutgoingCall.ts b/packages/communication-ui/src/hooks/useOutgoingCall.ts index dcbbff42e40..e43bdc61d2c 100644 --- a/packages/communication-ui/src/hooks/useOutgoingCall.ts +++ b/packages/communication-ui/src/hooks/useOutgoingCall.ts @@ -21,9 +21,9 @@ export const useOutgoingCall = (): UseOutgoingCallType => { setCallState(call?.state ?? 'None'); - call?.on('callStateChanged', updateCallState); + call?.on('stateChanged', updateCallState); return () => { - call?.off('callStateChanged', updateCallState); + call?.off('stateChanged', updateCallState); }; }, [call, setCallState]); @@ -43,7 +43,8 @@ export const useOutgoingCall = (): UseOutgoingCallType => { }; } - const newCall = callAgent.call([receiver], { videoOptions, audioOptions }); + const newCall = callAgent.startCall([receiver], { videoOptions, audioOptions }); + console.log('newCall', newCall); setCall(newCall); // Listen to Remote Participant screen share stream @@ -51,8 +52,8 @@ export const useOutgoingCall = (): UseOutgoingCallType => { newCall.remoteParticipants.forEach((participant) => { participant.on('videoStreamsUpdated', (e) => { e.added.forEach((addedStream) => { - if (addedStream.type === 'Video') return; - addedStream.on('availabilityChanged', () => { + if (addedStream.mediaStreamType === 'Video') return; + addedStream.on('isAvailableChanged', () => { if (addedStream.isAvailable) { setScreenShareStream({ stream: addedStream, user: participant }); } else { diff --git a/packages/communication-ui/src/hooks/useRemoteVideoStreamRenderer.test.ts b/packages/communication-ui/src/hooks/useRemoteVideoStreamRenderer.test.ts index 9a806232a6b..f587cbcdf49 100644 --- a/packages/communication-ui/src/hooks/useRemoteVideoStreamRenderer.test.ts +++ b/packages/communication-ui/src/hooks/useRemoteVideoStreamRenderer.test.ts @@ -6,7 +6,7 @@ import { PropertyChangedEvent, RemoteVideoStream } from '@azure/communication-ca jest.mock('@azure/communication-calling', () => { // Works and lets you check for constructor calls: return { - Renderer: jest.fn().mockImplementation(() => { + VideoStreamRenderer: jest.fn().mockImplementation(() => { return { createView: jest.fn().mockImplementation(() => { return { @@ -27,7 +27,7 @@ jest.mock('../providers/ErrorProvider', () => { const getRemoteVideoStreamStub = (isAvailable: boolean): RemoteVideoStream => { return { id: 1, - type: 'Video', + mediaStreamType: 'Video', isAvailable: isAvailable, on: () => { return; @@ -70,9 +70,9 @@ describe('useRemoteVideoStreamRenderer tests', () => { }; const { result } = renderHook(() => useRemoteVideoStreamRenderer(remoteVideoStream)); - expect(remoteVideoStreamEvents.availabilityChanged).toBeDefined(); + expect(remoteVideoStreamEvents.isAvailableChanged).toBeDefined(); - remoteVideoStreamEvents.availabilityChanged(); + remoteVideoStreamEvents.isAvailableChanged(); expect(result.current.isAvailable).toBe(false); }); diff --git a/packages/communication-ui/src/hooks/useRemoteVideoStreamRenderer.ts b/packages/communication-ui/src/hooks/useRemoteVideoStreamRenderer.ts index 574f8287971..c77cf2c9cb6 100644 --- a/packages/communication-ui/src/hooks/useRemoteVideoStreamRenderer.ts +++ b/packages/communication-ui/src/hooks/useRemoteVideoStreamRenderer.ts @@ -1,5 +1,10 @@ // © Microsoft Corporation. All rights reserved. -import { RemoteVideoStream, Renderer, RendererOptions, RendererView } from '@azure/communication-calling'; +import { + RemoteVideoStream, + VideoStreamRenderer, + CreateViewOptions, + VideoStreamRendererView +} from '@azure/communication-calling'; import { useEffect, useState, useRef } from 'react'; import { CommunicationUiErrorCode, CommunicationUiError } from '../types/CommunicationUiError'; import { useTriggerOnErrorCallback } from '../providers/ErrorProvider'; @@ -14,17 +19,17 @@ export type UseRemoteVideoStreamType = { // It also has an event handler to say when the stream is available or not since it can be difficult to tell from the returned HTMLElement. export default ( stream: RemoteVideoStream | undefined, - options?: RendererOptions | undefined + options?: CreateViewOptions | undefined ): UseRemoteVideoStreamType => { const onErrorCallback = useTriggerOnErrorCallback(); const [render, setRender] = useState(null); const [isAvailable, setIsAvailable] = useState(false); - const rendererViewRef: React.MutableRefObject = useRef(null); + const rendererViewRef: React.MutableRefObject = useRef(null); useEffect(() => { const renderStream = async ( stream: RemoteVideoStream | undefined, - renderViewRef: RendererView | null + renderViewRef: VideoStreamRendererView | null ): Promise => { if (!stream) { setRender(null); @@ -32,7 +37,7 @@ export default ( } if (stream && stream.isAvailable) { if (render === null) { - const renderer = new Renderer(stream); + const renderer = new VideoStreamRenderer(stream); try { renderViewRef = await renderer.createView(options); } catch (error) { @@ -72,10 +77,10 @@ export default ( propagateError(error, onErrorCallback); } }; - stream?.on('availabilityChanged', onAvailabilityChanged); + stream?.on('isAvailableChanged', onAvailabilityChanged); return () => { - stream?.off('availabilityChanged', onAvailabilityChanged); + stream?.off('isAvailableChanged', onAvailabilityChanged); }; }, [stream, options, render, rendererViewRef, onErrorCallback]); diff --git a/packages/communication-ui/src/hooks/useSubscribeToAudioDeviceList.test.ts b/packages/communication-ui/src/hooks/useSubscribeToAudioDeviceList.test.ts index e398843fb48..b42e2b5a35c 100644 --- a/packages/communication-ui/src/hooks/useSubscribeToAudioDeviceList.test.ts +++ b/packages/communication-ui/src/hooks/useSubscribeToAudioDeviceList.test.ts @@ -3,7 +3,7 @@ import { act, renderHook } from '@testing-library/react-hooks'; import { AudioDeviceInfo, DeviceManager } from '@azure/communication-calling'; import useSubscribeToAudioDeviceList from './useSubscribeToAudioDeviceList'; -import { createSpyObj } from '../mocks'; +import { createSpyObj, waitWithBreakCondition } from '../mocks'; import { useCallingContext } from '../providers'; jest.mock('@azure/communication-calling', () => { @@ -55,8 +55,8 @@ describe('useSubscribeToAudioDeviceList tests', () => { beforeEach(() => { callbacks = {}; const deviceManagerMock = createSpyObj('deviceManagerMock', [ - 'getMicrophoneList', - 'setMicrophone', + 'getMicrophones', + 'selectMicrophone', 'on', 'off' ]); @@ -84,7 +84,7 @@ describe('useSubscribeToAudioDeviceList tests', () => { deviceManagerMock.off.mockImplementation(() => { return; }); - deviceManagerMock.getMicrophoneList.mockImplementation(() => { + deviceManagerMock.getMicrophones.mockImplementation(async () => { return mockMicrophoneList; }); }); @@ -121,6 +121,7 @@ describe('useSubscribeToAudioDeviceList tests', () => { const callback = callbacks.audioDevicesUpdated; expect(callback).toBeTruthy(); act(callback); + await waitWithBreakCondition(() => useCallingContext().audioDeviceList !== undefined); expect(useCallingContext().audioDeviceList).toBe(mockMicrophoneList); }); @@ -129,6 +130,7 @@ describe('useSubscribeToAudioDeviceList tests', () => { const callback = callbacks.audioDevicesUpdated; expect(callback).toBeTruthy(); act(callback); + await waitWithBreakCondition(() => useCallingContext().audioDeviceInfo !== undefined); expect(useCallingContext().audioDeviceInfo).toBe(mockPrimaryMicrophone); }); }); diff --git a/packages/communication-ui/src/hooks/useSubscribeToAudioDeviceList.ts b/packages/communication-ui/src/hooks/useSubscribeToAudioDeviceList.ts index c09476d788a..eba461c7452 100644 --- a/packages/communication-ui/src/hooks/useSubscribeToAudioDeviceList.ts +++ b/packages/communication-ui/src/hooks/useSubscribeToAudioDeviceList.ts @@ -9,8 +9,8 @@ export default (): void => { useEffect(() => { if (!deviceManager || audioDevicePermission !== 'Granted') return; - function promptAudioDevices(deviceManager: DeviceManager): void { - const microphoneList: AudioDeviceInfo[] = deviceManager.getMicrophoneList(); + async function promptAudioDevices(deviceManager: DeviceManager): Promise { + const microphoneList: AudioDeviceInfo[] = await deviceManager.getMicrophones(); setAudioDeviceList(microphoneList); if (microphoneList.length > 0) setAudioDeviceInfo(microphoneList[0]); } diff --git a/packages/communication-ui/src/hooks/useSubscribeToDevicePermission.test.ts b/packages/communication-ui/src/hooks/useSubscribeToDevicePermission.test.ts index e7756c15bf7..37c8794af8e 100644 --- a/packages/communication-ui/src/hooks/useSubscribeToDevicePermission.test.ts +++ b/packages/communication-ui/src/hooks/useSubscribeToDevicePermission.test.ts @@ -1,11 +1,12 @@ // © Microsoft Corporation. All rights reserved. import { renderHook } from '@testing-library/react-hooks'; -import { DeviceAccess, DeviceManager, PermissionState } from '@azure/communication-calling'; +import { DeviceAccess, DeviceManager } from '@azure/communication-calling'; import useSubscribeToDevicePermission from './useSubscribeToDevicePermission'; -import { CallingProvider } from '../providers'; import React from 'react'; import { createSpyObj } from '../mocks'; +import { act } from 'react-dom/test-utils'; +import { MockCallingProvider } from '../mocks/MockCallingProvider'; jest.mock('@azure/communication-calling', () => { return { @@ -33,17 +34,7 @@ describe('useSubscribeToDevicePermission tests', () => { beforeEach(() => { setAudioDevicePermissionCallback = jest.fn(); setVideoDevicePermissionCallback = jest.fn(); - tsDeviceManagerMock = createSpyObj('tsDeviceManagerMock', [ - 'getPermissionState', - 'askDevicePermission', - 'on', - 'off' - ]); - tsDeviceManagerMock.getPermissionState.mockImplementation( - (): Promise => { - return Promise.resolve('Unknown'); - } - ); + tsDeviceManagerMock = createSpyObj('tsDeviceManagerMock', ['askDevicePermission', 'on', 'off']); tsDeviceManagerMock.askDevicePermission.mockImplementation( (): Promise => { return Promise.resolve({ audio: true, video: true }); @@ -74,65 +65,64 @@ describe('useSubscribeToDevicePermission tests', () => { expect(() => { renderHook(() => useSubscribeToDevicePermission('Camera'), { - wrapper: CallingProvider + wrapper: MockCallingProvider }).result.current; }).toThrowError(new Error('CallingContext is undefined')); }); test('useSubscribeToDevicePermission hook should ask user for permission if permission state is unknown', async () => { const { waitForNextUpdate } = renderHook(() => useSubscribeToDevicePermission('Camera'), { - wrapper: CallingProvider + wrapper: MockCallingProvider }); await waitForNextUpdate(); - expect(tsDeviceManagerMock.getPermissionState).toBeCalled(); expect(tsDeviceManagerMock.askDevicePermission).toBeCalled(); }); - test('useSubscribeToDevicePermission hook should ask user for permission if permission state is Prompt', async () => { - tsDeviceManagerMock.getPermissionState.mockImplementation( - (): Promise => { - return Promise.resolve('Prompt'); + test('useSubscribeToDevicePermission hook should not ask user for permission if permission state is Granted', async () => { + // 1. First call will ask for permission + tsDeviceManagerMock.askDevicePermission.mockImplementation( + (): Promise => { + return Promise.resolve({ audio: true, video: true }); } ); - const { waitForNextUpdate } = renderHook(() => useSubscribeToDevicePermission('Camera'), { - wrapper: CallingProvider + const { waitForNextUpdate, rerender } = renderHook(() => useSubscribeToDevicePermission('Camera'), { + wrapper: MockCallingProvider }); await waitForNextUpdate(); - expect(tsDeviceManagerMock.getPermissionState).toBeCalled(); expect(tsDeviceManagerMock.askDevicePermission).toBeCalled(); - }); - test('useSubscribeToDevicePermission hook should not ask user for permission if permission state is Granted', async () => { - tsDeviceManagerMock.getPermissionState.mockImplementation( - (): Promise => { - return Promise.resolve('Granted'); - } - ); - const { waitForNextUpdate } = renderHook(() => useSubscribeToDevicePermission('Camera'), { - wrapper: CallingProvider + // Second call will not ask for permission as it was already denied + tsDeviceManagerMock.askDevicePermission.mockClear(); + act(() => { + rerender(); }); - await waitForNextUpdate(); - expect(tsDeviceManagerMock.getPermissionState).toBeCalled(); expect(tsDeviceManagerMock.askDevicePermission).not.toBeCalled(); }); test('useSubscribeToDevicePermission hook should not ask user for permission if permission state is Denied', async () => { - tsDeviceManagerMock.getPermissionState.mockImplementation( - (): Promise => { - return Promise.resolve('Denied'); + // 1. First call will ask for permission + tsDeviceManagerMock.askDevicePermission.mockImplementation( + (): Promise => { + return Promise.resolve({ audio: false, video: false }); } ); - const { waitForNextUpdate } = renderHook(() => useSubscribeToDevicePermission('Camera'), { - wrapper: CallingProvider + const { waitForNextUpdate, rerender } = renderHook(() => useSubscribeToDevicePermission('Camera'), { + wrapper: MockCallingProvider }); await waitForNextUpdate(); - expect(tsDeviceManagerMock.getPermissionState).toBeCalled(); + expect(tsDeviceManagerMock.askDevicePermission).toBeCalled(); + + // Second call will not ask for permission as it was already denied + tsDeviceManagerMock.askDevicePermission.mockClear(); + act(() => { + rerender(); + }); expect(tsDeviceManagerMock.askDevicePermission).not.toBeCalled(); }); test('useSubscribeToDevicePermission hook should set videoDevicePermission with Granted if video permission is true', async () => { const { waitForNextUpdate } = renderHook(() => useSubscribeToDevicePermission('Camera'), { - wrapper: CallingProvider + wrapper: MockCallingProvider }); await waitForNextUpdate(); expect(setVideoDevicePermissionCallback).toBeCalledWith('Granted'); @@ -142,11 +132,11 @@ describe('useSubscribeToDevicePermission tests', () => { test('useSubscribeToDevicePermission hook should set videoDevicePermission with Denied if video permission is false', async () => { tsDeviceManagerMock.askDevicePermission.mockImplementation( (): Promise => { - return Promise.resolve({ video: false }); + return Promise.resolve({ audio: false, video: false }); } ); const { waitForNextUpdate } = renderHook(() => useSubscribeToDevicePermission('Camera'), { - wrapper: CallingProvider + wrapper: MockCallingProvider }); await waitForNextUpdate(); expect(setVideoDevicePermissionCallback).toBeCalledWith('Denied'); @@ -155,7 +145,7 @@ describe('useSubscribeToDevicePermission tests', () => { test('useSubscribeToDevicePermission hook should set audioDevicePermission with Granted if audio permission is true', async () => { const { waitForNextUpdate } = renderHook(() => useSubscribeToDevicePermission('Microphone'), { - wrapper: CallingProvider + wrapper: MockCallingProvider }); await waitForNextUpdate(); expect(setAudioDevicePermissionCallback).toBeCalledWith('Granted'); @@ -165,11 +155,11 @@ describe('useSubscribeToDevicePermission tests', () => { test('useSubscribeToDevicePermission hook should set audioDevicePermission with Denied if audio permission is false', async () => { tsDeviceManagerMock.askDevicePermission.mockImplementation( (): Promise => { - return Promise.resolve({ audio: false }); + return Promise.resolve({ audio: false, video: false }); } ); const { waitForNextUpdate } = renderHook(() => useSubscribeToDevicePermission('Microphone'), { - wrapper: CallingProvider + wrapper: MockCallingProvider }); await waitForNextUpdate(); expect(setAudioDevicePermissionCallback).toBeCalledWith('Denied'); diff --git a/packages/communication-ui/src/hooks/useSubscribeToDevicePermission.ts b/packages/communication-ui/src/hooks/useSubscribeToDevicePermission.ts index 1f8c82d454c..f3d51a4cc68 100644 --- a/packages/communication-ui/src/hooks/useSubscribeToDevicePermission.ts +++ b/packages/communication-ui/src/hooks/useSubscribeToDevicePermission.ts @@ -1,16 +1,16 @@ // © Microsoft Corporation. All rights reserved. -import { PermissionType, PermissionState as DevicePermissionState, DeviceAccess } from '@azure/communication-calling'; import { CallingContext } from '../providers'; -import { useContext, useEffect, useState } from 'react'; +import { useContext, useEffect, useRef, useState } from 'react'; import { CommunicationUiErrorCode, CommunicationUiError } from '../types/CommunicationUiError'; import { useTriggerOnErrorCallback } from '../providers/ErrorProvider'; import { propagateError } from '../utils/SDKUtils'; +import { DevicePermissionState, DevicePermissionType } from '../types/DevicePermission'; // uses the device manager which abstracts away the HTML5 permission system // device manager will be able to get the initial real state // at some time later (useEffect) we will have the real default value // if the real value was prompt, then we can call askPermission again to get a Granted or Denied value -export default (permissionType: PermissionType): void => { +export default (permissionType: DevicePermissionType): void => { const context = useContext(CallingContext); const onErrorCallback = useTriggerOnErrorCallback(); if (!context) { @@ -20,29 +20,43 @@ export default (permissionType: PermissionType): void => { }); } const { deviceManager, setAudioDevicePermission, setVideoDevicePermission } = context; - const [permissionState, setPermissionState] = useState('Unknown'); + const [devicePermissionState, setDevicePermissionState] = useState('Unknown'); + const mounted = useRef(false); + // With new SDK, the permissions all happen async. Make sure not up try to update state if the component was already + // unmounted. useEffect(() => { - if (!deviceManager || permissionState === 'Granted' || permissionState === 'Denied') return; + mounted.current = true; + return () => { + mounted.current = false; + }; + }); + + useEffect(() => { + if (!deviceManager || devicePermissionState === 'Granted' || devicePermissionState === 'Denied') return; const queryPermissionState = async (): Promise => { - let state: DevicePermissionState; - try { - state = await deviceManager.getPermissionState(permissionType); - } catch (error) { - throw new CommunicationUiError({ - message: 'Error getting permission state', - code: CommunicationUiErrorCode.QUERY_PERMISSIONS_ERROR, - error: error - }); - } - if (state === 'Unknown' || state === 'Prompt') { - let access: DeviceAccess; + if (devicePermissionState === 'Unknown') { try { - access = await deviceManager.askDevicePermission( - permissionType === 'Microphone', - permissionType === 'Camera' - ); + if (permissionType === 'Microphone') { + const access = await deviceManager.askDevicePermission({ video: false, audio: true }); + if (!mounted.current) { + return; + } + const permissionState = access.audio ? 'Granted' : 'Denied'; + setDevicePermissionState(permissionState); + setAudioDevicePermission(permissionState); + } else if (permissionType === 'Camera') { + const access = await deviceManager.askDevicePermission({ video: true, audio: false }); + if (!mounted.current) { + return; + } + const permissionState = access.video ? 'Granted' : 'Denied'; + setDevicePermissionState(permissionState); + setVideoDevicePermission(permissionState); + } else { + throw new Error('invalid device type specified'); + } } catch (error) { throw new CommunicationUiError({ message: 'Error asking permissions', @@ -50,34 +64,18 @@ export default (permissionType: PermissionType): void => { error }); } - if (permissionType === 'Camera') { - state = access.video ? 'Granted' : 'Denied'; - } - if (permissionType === 'Microphone') { - state = access.audio ? 'Granted' : 'Denied'; - } } - setPermissionState(state); - - if (permissionType === 'Camera') setVideoDevicePermission(state); - else setAudioDevicePermission(state); }; queryPermissionState().catch((error) => { propagateError(error, onErrorCallback); }); - - deviceManager.on('permissionStateChanged', queryPermissionState); - - return () => { - deviceManager.off('permissionStateChanged', queryPermissionState); - }; }, [ deviceManager, permissionType, - permissionState, setAudioDevicePermission, setVideoDevicePermission, - onErrorCallback + onErrorCallback, + devicePermissionState ]); }; diff --git a/packages/communication-ui/src/hooks/useSubscribeToVideoDeviceList.test.ts b/packages/communication-ui/src/hooks/useSubscribeToVideoDeviceList.test.ts index 5187a930a4b..8fb00559240 100644 --- a/packages/communication-ui/src/hooks/useSubscribeToVideoDeviceList.test.ts +++ b/packages/communication-ui/src/hooks/useSubscribeToVideoDeviceList.test.ts @@ -3,7 +3,7 @@ import { act, renderHook } from '@testing-library/react-hooks'; import { DeviceManager, VideoDeviceInfo } from '@azure/communication-calling'; import useSubscribeToVideoDeviceList from './useSubscribeToVideoDeviceList'; -import { createSpyObj } from '../mocks'; +import { createSpyObj, waitWithBreakCondition } from '../mocks'; import { useCallingContext } from '../providers'; jest.mock('@azure/communication-calling', () => { @@ -45,14 +45,14 @@ describe('useSubscribeToVideoDeviceList tests', () => { beforeEach(() => { callbacks = {}; - const deviceManagerMock = createSpyObj('deviceManagerMock', ['getCameraList', 'on', 'off']); + const deviceManagerMock = createSpyObj('deviceManagerMock', ['getCameras', 'on', 'off']); deviceManagerMock.on.mockImplementation((name: string, callback: () => void) => { callbacks[name] = callback; }); deviceManagerMock.off.mockImplementation(() => { return; }); - deviceManagerMock.getCameraList.mockImplementation(() => { + deviceManagerMock.getCameras.mockImplementation(async () => { return mockCameraList; }); @@ -95,6 +95,7 @@ describe('useSubscribeToVideoDeviceList tests', () => { const callback = callbacks.videoDevicesUpdated; expect(callback).toBeTruthy(); act(callback); + await waitWithBreakCondition(() => useCallingContext().videoDeviceList !== undefined); expect(useCallingContext().videoDeviceList).toBe(mockCameraList); }); @@ -103,6 +104,7 @@ describe('useSubscribeToVideoDeviceList tests', () => { const callback = callbacks.videoDevicesUpdated; expect(callback).toBeTruthy(); act(callback); + await waitWithBreakCondition(() => useCallingContext().videoDeviceInfo !== undefined); expect(useCallingContext().videoDeviceInfo).toBe(mockFrontCamera); }); }); diff --git a/packages/communication-ui/src/hooks/useSubscribeToVideoDeviceList.ts b/packages/communication-ui/src/hooks/useSubscribeToVideoDeviceList.ts index af3db498b69..5e570790ce2 100644 --- a/packages/communication-ui/src/hooks/useSubscribeToVideoDeviceList.ts +++ b/packages/communication-ui/src/hooks/useSubscribeToVideoDeviceList.ts @@ -16,8 +16,8 @@ export default (): void => { useEffect(() => { if (!deviceManager || videoDevicePermission !== 'Granted') return; - function promptVideoDevices(deviceManager: DeviceManager): void { - const cameraList: VideoDeviceInfo[] = deviceManager.getCameraList(); + async function promptVideoDevices(deviceManager: DeviceManager): Promise { + const cameraList: VideoDeviceInfo[] = await deviceManager.getCameras(); setVideoDeviceList(cameraList); //Reset if the selected video is no longer available or no selected video. diff --git a/packages/communication-ui/src/hooks/useTeamsCall.ts b/packages/communication-ui/src/hooks/useTeamsCall.ts index 0a8ebd45d34..0f1d5b5192b 100644 --- a/packages/communication-ui/src/hooks/useTeamsCall.ts +++ b/packages/communication-ui/src/hooks/useTeamsCall.ts @@ -1,12 +1,12 @@ // © Microsoft Corporation. All rights reserved. -import { AudioOptions, Call, HangupCallOptions, JoinCallOptions } from '@azure/communication-calling'; +import { AudioOptions, Call, HangUpOptions, JoinCallOptions } from '@azure/communication-calling'; import { useCallback } from 'react'; import { CommunicationUiErrorCode, CommunicationUiError } from '../types/CommunicationUiError'; import { useCallingContext, useCallContext } from '../providers'; export type UseTeamsCallType = { - leave: (hangupCallOptions: HangupCallOptions) => Promise; + leave: (hangupCallOptions: HangUpOptions) => Promise; join: (meetingLink: string, joinCallOptions?: JoinCallOptions) => Call; }; @@ -42,7 +42,7 @@ export const useTeamsCall = (): UseTeamsCallType => { ); const leave = useCallback( - async (hangupCallOptions: HangupCallOptions): Promise => { + async (hangupCallOptions: HangUpOptions): Promise => { if (!call) { throw new CommunicationUiError({ message: 'Call is invalid', diff --git a/packages/communication-ui/src/mocks/CallingMocks.ts b/packages/communication-ui/src/mocks/CallingMocks.ts index b1d818e4b2b..67e23ddd83e 100644 --- a/packages/communication-ui/src/mocks/CallingMocks.ts +++ b/packages/communication-ui/src/mocks/CallingMocks.ts @@ -1,6 +1,7 @@ // © Microsoft Corporation. All rights reserved. import { + CallApiFeature, LocalVideoStream, MediaStreamType, RemoteParticipant, @@ -46,13 +47,13 @@ export const defaultMockCallProps = { export function mockCall(mockProps?: MockCallProps): MockCall { const props = { ...defaultMockCallProps, ...mockProps }; + return { id: 'call id', - callerIdentity: undefined, + callerInfo: { identifier: undefined }, state: 'None', - isIncoming: props.isIncoming, - isMicrophoneMuted: props.isMicrophoneMuted, - isRecordingActive: false, + direction: props.isIncoming ? 'Incoming' : 'Outgoing', + isMuted: props.isMicrophoneMuted, isScreenSharingOn: props.isScreenSharingOn, localVideoStreams: props.localVideoStreams, remoteParticipants: [], @@ -66,21 +67,17 @@ export function mockCall(mockProps?: MockCallProps): MockCall { props.unmuteExecutedCallback(true); }); }, - accept: async () => { - return await new Promise((resolve) => resolve()).then(() => { - props.acceptExecutedCallback(true); - }); - }, - reject: async () => { - return await new Promise((resolve) => resolve()).then(() => { - props.rejectExecutedCallback(true); - }); + api: (): TFeature => { + throw new Error('not implemented'); }, hangUp: async () => { return await new Promise((resolve) => resolve()).then(() => { props.hangUpExecutedCallback(); }); }, + sendDtmf: async () => { + return await new Promise((resolve) => resolve()); + }, startVideo: async () => { props.startVideo(true); return await new Promise((resolve) => resolve()); @@ -91,7 +88,7 @@ export function mockCall(mockProps?: MockCallProps): MockCall { }, addParticipant: () => { const a: RemoteParticipant = { - identifier: { phoneNumber: 'phoneNumber' }, + identifier: { phoneNumber: 'phoneNumber', kind: 'phoneNumber' }, state: 'Connecting', videoStreams: [], isMuted: false, @@ -111,7 +108,7 @@ export function mockCall(mockProps?: MockCallProps): MockCall { hold: async () => { return await new Promise((resolve) => resolve()); }, - unhold: async () => { + resume: async () => { return await new Promise((resolve) => resolve()); }, startScreenSharing: async () => { @@ -129,9 +126,6 @@ export function mockCall(mockProps?: MockCallProps): MockCall { }, off: () => { return; - }, - sendDtmf: async () => { - return await new Promise((resolve) => resolve()); } }; } @@ -140,7 +134,7 @@ export function mockCallAgent(props?: MockCallProps): MockCallAgent { const call = mockCall(props); return { calls: [call], - call: () => { + startCall: () => { props?.outgoingCallExecutedCallback(); return call; }, @@ -156,9 +150,6 @@ export function mockCallAgent(props?: MockCallProps): MockCallAgent { }, dispose: async () => { return await new Promise((resolve) => resolve()); - }, - updateDisplayName: () => { - return; } }; } @@ -170,7 +161,7 @@ export function mockRemoteParticipant( isMuted?: boolean ): RemoteParticipant { return { - identifier: { communicationUserId: 'id' }, + identifier: { communicationUserId: 'id', kind: 'communicationUser' }, displayName: displayName ?? 'displayName', state: state ?? 'Connected', videoStreams: videoStreams ?? [], @@ -188,7 +179,7 @@ export function mockRemoteParticipant( export function mockRemoteVideoStream(type?: MediaStreamType, isAvailable?: boolean): MockRemoteVideoStream { return { id: 1, - type: type ?? 'Video', + mediaStreamType: type ?? 'Video', isAvailable: isAvailable ?? false, on: () => { return; diff --git a/packages/communication-ui/src/mocks/CallingTypeMocks.ts b/packages/communication-ui/src/mocks/CallingTypeMocks.ts index 94750ab45be..37097007fbd 100644 --- a/packages/communication-ui/src/mocks/CallingTypeMocks.ts +++ b/packages/communication-ui/src/mocks/CallingTypeMocks.ts @@ -1,23 +1,34 @@ // © Microsoft Corporation. All rights reserved. import { - AcceptCallOptions, AddPhoneNumberOptions, + Call, + CallApiFeature, + CallDirection, CallEndReason, + CallerInfo, + CallFeatureFactoryType, CallState, CollectionUpdatedEvent, DtmfTone, - GroupCallContext, - GroupChatCallContext, - HangupCallOptions, + GroupChatCallLocator, + GroupLocator, + HangUpOptions, + IncomingCallEvent, JoinCallOptions, LocalVideoStream, MediaStreamType, + MeetingLocator, PropertyChangedEvent, RemoteParticipant, StartCallOptions } from '@azure/communication-calling'; -import { CallingApplication, CommunicationUser, PhoneNumber, UnknownIdentifier } from '@azure/communication-common'; +import { + CommunicationUserIdentifier, + MicrosoftTeamsUserIdentifier, + PhoneNumberIdentifier, + UnknownIdentifier +} from '@azure/communication-common'; /** * Represents a MockCall @@ -27,58 +38,50 @@ export declare interface MockCall { /** * Get the unique Id for this Call. */ - readonly id: string; + id: string; /** - * The identity of caller if the call is incoming. + * Caller Information if the call is incoming. */ - readonly callerIdentity: CommunicationUser | PhoneNumber | CallingApplication | UnknownIdentifier | undefined; + callerInfo: CallerInfo; /** * Get the state of this Call. */ - readonly state: CallState; - /** - * Containing code/subcode indicating how call ended - */ - readonly callEndReason?: CallEndReason; + state: CallState; /** - * Whether this Call is incoming. + * Containing code/subCode indicating how call ended */ - isIncoming: boolean; + callEndReason?: CallEndReason; /** - * Whether this local microphone is muted. + * Get the call direction, whether Incoming or Outgoing. */ - isMicrophoneMuted: boolean; + direction: CallDirection; /** - * When the call is being actively recorded + * Whether local user is muted, locally or remotely */ - isRecordingActive: boolean; + isMuted: boolean; /** * Whether screen sharing is on */ - readonly isScreenSharingOn: boolean; + isScreenSharingOn: boolean; /** * Collection of video streams sent to other participants in a call. */ - readonly localVideoStreams: LocalVideoStream[]; + localVideoStreams: ReadonlyArray; /** * Collection of remote participants participating in this call. */ - remoteParticipants: RemoteParticipant[]; - - /** - * Accept this incoming Call. - * @param options - accept options. - */ - accept(options?: AcceptCallOptions): Promise; + remoteParticipants: ReadonlyArray; /** - * Reject this incoming Call. + * Retrieves an initialized and memoized API feature object with extended API. + * @param cls - The call feature class that provides an object with extended API. + * @beta */ - reject(): Promise; + api(cls: CallFeatureFactoryType): TFeature; /** * Hang up the call. - * @param options? - Hangup options. + * @param options? - HangUp options. */ - hangUp(options?: HangupCallOptions): Promise; + hangUp(options?: HangUpOptions): Promise; /** * Mute local microphone. */ @@ -88,7 +91,7 @@ export declare interface MockCall { */ unmute(): Promise; /** - * Unmute local microphone. + * Send DTMF tone. */ sendDtmf(dtmfTone: DtmfTone): Promise; /** @@ -104,27 +107,27 @@ export declare interface MockCall { /** * Add a participant to this Call. * @param identifier - The identifier of the participant to add. - * @param options - options + * @param options - Additional options for managing the PSTN call. For example, setting the Caller Id phone number in a PSTN call. * @returns The RemoteParticipant object associated with the successfully added participant. */ - addParticipant(identifier: CommunicationUser | CallingApplication): RemoteParticipant; - addParticipant(identifier: PhoneNumber, options?: AddPhoneNumberOptions): RemoteParticipant; + addParticipant(identifier: CommunicationUserIdentifier | MicrosoftTeamsUserIdentifier): RemoteParticipant; + addParticipant(identifier: PhoneNumberIdentifier, options?: AddPhoneNumberOptions): RemoteParticipant; /** * Remove a participant from this Call. * @param identifier - The identifier of the participant to remove. * @param options - options */ removeParticipant( - identifier: CommunicationUser | PhoneNumber | CallingApplication | UnknownIdentifier + identifier: CommunicationUserIdentifier | PhoneNumberIdentifier | MicrosoftTeamsUserIdentifier | UnknownIdentifier ): Promise; /** * Put this Call on hold. */ hold(): Promise; /** - * Unhold this Call. + * Resume this Call. */ - unhold(): Promise; + resume(): Promise; /** * Start local screen sharing, browser handles screen/window enumeration and selection. */ @@ -134,17 +137,23 @@ export declare interface MockCall { */ stopScreenSharing(): Promise; /** - * Subscribe function for callStateChanged event + * Subscribe function for stateChanged event * @param event - event name * @param listener - callback fn that will be called when value of this property will change */ - on(event: 'callStateChanged', listener: PropertyChangedEvent): void; + on(event: 'stateChanged', listener: PropertyChangedEvent): void; /** - * Subscribe function for callIdChanged event + * Subscribe function for idChanged event * @param event - event name * @param listener - callback fn that will be called when value of this property will change */ - on(event: 'callIdChanged', listener: PropertyChangedEvent): void; + on(event: 'idChanged', listener: PropertyChangedEvent): void; + /** + * Subscribe function for isMutedChanged event + * @param event - event name + * @param listener - callback fn that will be called when value of this property will change + */ + on(event: 'isMutedChanged', listener: PropertyChangedEvent): void; /** * Subscribe function for isScreenSharingChanged event * @param event - event name @@ -165,25 +174,24 @@ export declare interface MockCall { * it will pass arrays of added and removed elements */ on(event: 'localVideoStreamsUpdated', listener: CollectionUpdatedEvent): void; - /** - * Unsubscribe function for callStateChanged event + * Unsubscribe function for stateChanged event * @param event - event name * @param listener - callback fn that was used to subscribe to this event */ - on(event: 'isRecordingActiveChanged', listener: PropertyChangedEvent): void; + off(event: 'stateChanged', listener: PropertyChangedEvent): void; /** - * Unsubscribe function for callStateChanged event + * Unsubscribe function for idChanged event * @param event - event name * @param listener - callback fn that was used to subscribe to this event */ - off(event: 'callStateChanged', listener: PropertyChangedEvent): void; + off(event: 'idChanged', listener: PropertyChangedEvent): void; /** - * Unsubscribe function for callIdChanged event + * Subscribe function for isMutedChanged event * @param event - event name - * @param listener - callback fn that was used to subscribe to this event + * @param listener - callback fn that will be called when value of this property will change */ - off(event: 'callIdChanged', listener: PropertyChangedEvent): void; + off(event: 'isMutedChanged', listener: PropertyChangedEvent): void; /** * Unsubscribe function for isScreenSharingChanged event * @param event - event name @@ -202,13 +210,6 @@ export declare interface MockCall { * @param listener - callback fn that was used to subscribe to this event */ off(event: 'localVideoStreamsUpdated', listener: CollectionUpdatedEvent): void; - /** - * Unsubscribe function for isRecordingActiveChanged event - * @beta - * @param event - event name - * @param listener - callback fn that was used to subscribe to this event - */ - off(event: 'isRecordingActiveChanged', listener: PropertyChangedEvent): void; } /** @@ -223,7 +224,7 @@ export declare interface MockRemoteVideoStream { /** * Get this remote media stream type. */ - type: MediaStreamType; + mediaStreamType: MediaStreamType; /** * Whether the stream is available or not. */ @@ -233,7 +234,7 @@ export declare interface MockRemoteVideoStream { * @param event - event name * @param listener - callback fn that will be called when value of this property will change */ - on(event: 'availabilityChanged', listener: PropertyChangedEvent): void; + on(event: 'isAvailableChanged', listener: PropertyChangedEvent): void; /** * Subscribe function for activeRenderersChanged event * @param event - event name @@ -245,7 +246,7 @@ export declare interface MockRemoteVideoStream { * @param event - event name * @param listener - callback fn that was used to subscribe to this event */ - off(event: 'availabilityChanged', listener: PropertyChangedEvent): void; + off(event: 'isAvailableChanged', listener: PropertyChangedEvent): void; } /** @@ -256,48 +257,73 @@ export declare interface MockCallAgent { /** * Get the calls. */ - calls: MockCall[]; + readonly calls: ReadonlyArray; + /** + * Specify the display name of the local participant for all new calls. + */ + readonly displayName?: string; /** * Initiates a call to the participants provided. * @param participants[] - User Identifiers (Callees) to make a call to. * @param options? - Start Call options. * @returns The Call object associated with the started call. */ - call( - participants: (CommunicationUser | PhoneNumber | CallingApplication | UnknownIdentifier)[], + startCall( + participants: (CommunicationUserIdentifier | PhoneNumberIdentifier | UnknownIdentifier)[], options?: StartCallOptions - ): MockCall; + ): Call; /** * Join a group call. * To join a group call just use a groupId. - * @param context - Group call context information. + * @param groupLocator - Group call information. * @param options - Call start options. * @returns The Call object associated with the call. */ - join(context: GroupChatCallContext, options?: JoinCallOptions): MockCall; - join(context: GroupCallContext, options?: JoinCallOptions): MockCall; - + join(groupLocator: GroupLocator, options?: JoinCallOptions): Call; /** - * Update display name of local participant. - * It will be used in all new calls. - * @param displayName The display name to use. + * Join a group chat call. + * To join a group chat call just use a threadId. + * @param groupChatCallLocator - GroupChat call information. + * @param options - Call start options. + * @returns The Call object associated with the call. + * @beta */ - updateDisplayName(displayName: string): void; + join(groupChatCallLocator: GroupChatCallLocator, options?: JoinCallOptions): Call; + /** + * Join a meeting. + * @param meetingLocator - Meeting information. + * @param options - Call start options. + * @returns The Call object associated with the call. + * @beta + */ + join(meetingLocator: MeetingLocator, options?: JoinCallOptions): Call; /** * Dispose this CallAgent ( required to create another new CallAgent) */ dispose(): Promise; + /** + * Subscribe function for incomingCall event. + * @param event - event name + * @param listener - callback fn that will be called when this callAgent will receive an incoming call + */ + on(event: 'incomingCall', listener: IncomingCallEvent): void; /** * Subscribe function for callsUpdated event. * @param event - event name * @param listener - callback fn that will be called when this collection will change, * it will pass arrays of added and removed elements */ - on(event: 'callsUpdated', listener: CollectionUpdatedEvent): void; + on(event: 'callsUpdated', listener: CollectionUpdatedEvent): void; + /** + * Unsubscribe function for incomingCall event. + * @param event - event name. + * @param listener - callback fn that was used to subscribe to this event. + */ + off(event: 'incomingCall', listener: IncomingCallEvent): void; /** * Unsubscribe function for callsUpdated event. * @param event - event name. * @param listener - allback fn that was used to subscribe to this event. */ - off(event: 'callsUpdated', listener: CollectionUpdatedEvent): void; + off(event: 'callsUpdated', listener: CollectionUpdatedEvent): void; } diff --git a/packages/communication-ui/src/mocks/MockCallingProvider.tsx b/packages/communication-ui/src/mocks/MockCallingProvider.tsx new file mode 100644 index 00000000000..45ed14ec868 --- /dev/null +++ b/packages/communication-ui/src/mocks/MockCallingProvider.tsx @@ -0,0 +1,70 @@ +// © Microsoft Corporation. All rights reserved. + +import { CallingContext, CallingContextType } from '../providers/CallingProvider'; +import React, { useState } from 'react'; +import { + AudioDeviceInfo, + CallAgent, + CallClient, + CallClientOptions, + DeviceManager, + VideoDeviceInfo +} from '@azure/communication-calling'; +import { DevicePermissionState } from '../types/DevicePermission'; +import { ErrorHandlingProps } from '../providers/ErrorProvider'; +import { AbortSignalLike } from '@azure/core-http'; + +interface CallingProviderProps { + children: React.ReactNode; + token: string; + displayName: string; + callClientOptions?: CallClientOptions; + refreshTokenCallback?: (abortSignal?: AbortSignalLike) => Promise; +} + +/** + * MockCallingProvider just provides some empty default values and default functions to avoid the useEffect being called + * in tests. + * + * @param props + * @returns + */ +export const MockCallingProvider = (props: CallingProviderProps & ErrorHandlingProps): JSX.Element => { + const [callClient, setCallClient] = useState(new CallClient()); + const [callAgent, setCallAgent] = useState(undefined); + const [deviceManager, setDeviceManager] = useState(undefined); + const [userId, setUserId] = useState(''); + const [displayName, setDisplayName] = useState(''); + const [audioDevicePermission, setAudioDevicePermission] = useState('Unknown'); + const [videoDevicePermission, setVideoDevicePermission] = useState('Unknown'); + const [videoDeviceInfo, setVideoDeviceInfo] = useState(undefined); + const [videoDeviceList, setVideoDeviceList] = useState([]); + const [audioDeviceInfo, setAudioDeviceInfo] = useState(undefined); + const [audioDeviceList, setAudioDeviceList] = useState([]); + + const initialState: CallingContextType = { + callClient, + setCallClient, + callAgent, + setCallAgent, + deviceManager, + setDeviceManager, + userId, + setUserId, + displayName, + setDisplayName, + audioDevicePermission, + setAudioDevicePermission, + videoDevicePermission, + setVideoDevicePermission, + videoDeviceInfo, + setVideoDeviceInfo, + videoDeviceList, + setVideoDeviceList, + audioDeviceInfo, + setAudioDeviceInfo, + audioDeviceList, + setAudioDeviceList + }; + return {props.children}; +}; diff --git a/packages/communication-ui/src/mocks/MockUtils.ts b/packages/communication-ui/src/mocks/MockUtils.ts index 2def3a78c04..8bf29753ae5 100644 --- a/packages/communication-ui/src/mocks/MockUtils.ts +++ b/packages/communication-ui/src/mocks/MockUtils.ts @@ -7,3 +7,27 @@ export function createSpyObj(baseName: string, methodNames: (keyof T)[]): jes } return obj; } + +function waitMilliseconds(duration: number): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, duration); + }); +} + +/** + * This will wait for up to 4 seconds and break when the given breakCondition is true. The reason for four seconds is + * that by default the jest timeout for waiting for test is 5 seconds so ideally we want to break this and fail then + * fail some expects check before the 5 seconds otherwise you'll just get a cryptic 'jest timeout error'. + * + * @param breakCondition + */ +export async function waitWithBreakCondition(breakCondition: () => boolean): Promise { + for (let i = 0; i < 40; i++) { + await waitMilliseconds(100); + if (breakCondition()) { + break; + } + } +} diff --git a/packages/communication-ui/src/providers/CallProvider.tsx b/packages/communication-ui/src/providers/CallProvider.tsx index e97a4d735c0..cdebafd0ad5 100644 --- a/packages/communication-ui/src/providers/CallProvider.tsx +++ b/packages/communication-ui/src/providers/CallProvider.tsx @@ -1,12 +1,11 @@ // © Microsoft Corporation. All rights reserved. -import React, { createContext, useState, Dispatch, SetStateAction, useEffect } from 'react'; +import React, { createContext, useState, Dispatch, SetStateAction } from 'react'; import { Call, CallState, LocalVideoStream, RemoteParticipant } from '@azure/communication-calling'; import { ParticipantStream } from '../types/ParticipantStream'; import { useValidContext } from '../utils'; import { WithErrorHandling } from '../utils/WithErrorHandling'; import { ErrorHandlingProps } from './ErrorProvider'; -import { useCallingContext } from './CallingProvider'; export type CallContextType = { call: Call | undefined; @@ -17,8 +16,6 @@ export type CallContextType = { setParticipants: Dispatch>; screenShareStream: ParticipantStream | undefined; setScreenShareStream: Dispatch>; - displayName: string; // can remove when we update to 1.0.0-beta.3 - setDisplayName: Dispatch>; // can remove when we update to 1.0.0-beta.3 isMicrophoneEnabled: boolean; setIsMicrophoneEnabled: Dispatch>; localScreenShareActive: boolean; @@ -33,13 +30,12 @@ export type CallContextType = { export interface CallProvider { children: React.ReactNode; - displayName: string; } export const CallContext = createContext(undefined); const CallProviderBase = (props: CallProvider): JSX.Element => { - const { displayName: defaultDisplayName, children } = props; + const { children } = props; const [call, setCall] = useState(undefined); const [callState, setCallState] = useState('None'); @@ -49,14 +45,7 @@ const CallProviderBase = (props: CallProvider): JSX.Element => { const [localScreenShareActive, setLocalScreenShare] = useState(false); const [localVideoStream, setLocalVideoStream] = useState(undefined); const [isLocalVideoRendererBusy, setLocalVideoRendererBusy] = useState(false); - const [displayName, setDisplayName] = useState(defaultDisplayName); // can remove when we update to 1.0.0-beta.3 const [isLocalVideoOn, setLocalVideoOn] = useState(false); - const { callAgent } = useCallingContext(); - - // this will be not needed once we update to beta3, a little bit ugly now that we have two useEffect - useEffect(() => { - callAgent?.updateDisplayName(displayName); - }, [displayName, callAgent]); const initialState: CallContextType = { call, @@ -71,8 +60,6 @@ const CallProviderBase = (props: CallProvider): JSX.Element => { setIsMicrophoneEnabled, localScreenShareActive, setLocalScreenShare, - displayName, // can remove when we update to 1.0.0-beta.3 - setDisplayName, // can remove when we update to 1.0.0-beta.3 localVideoStream, setLocalVideoStream, isLocalVideoRendererBusy, diff --git a/packages/communication-ui/src/providers/CallingProvider.tsx b/packages/communication-ui/src/providers/CallingProvider.tsx index 7b356b6cee0..5b77d350262 100644 --- a/packages/communication-ui/src/providers/CallingProvider.tsx +++ b/packages/communication-ui/src/providers/CallingProvider.tsx @@ -5,7 +5,6 @@ import { CallClient, CallAgent, DeviceManager, - PermissionState, VideoDeviceInfo, AudioDeviceInfo, CallClientOptions @@ -15,20 +14,23 @@ import { AbortSignalLike } from '@azure/core-http'; import { ErrorHandlingProps } from './ErrorProvider'; import { WithErrorHandling } from '../utils/WithErrorHandling'; import { CommunicationUiError, CommunicationUiErrorCode } from '../types/CommunicationUiError'; +import { DevicePermissionState } from '../types/DevicePermission'; export type CallingContextType = { userId: string; setUserId: Dispatch>; + displayName: string; + setDisplayName: Dispatch>; callClient: CallClient; setCallClient: Dispatch>; callAgent: CallAgent | undefined; setCallAgent: Dispatch>; deviceManager: DeviceManager | undefined; setDeviceManager: Dispatch>; - audioDevicePermission: PermissionState; - setAudioDevicePermission: Dispatch>; - videoDevicePermission: PermissionState; - setVideoDevicePermission: Dispatch>; + audioDevicePermission: DevicePermissionState; + setAudioDevicePermission: Dispatch>; + videoDevicePermission: DevicePermissionState; + setVideoDevicePermission: Dispatch>; videoDeviceInfo: VideoDeviceInfo | undefined; setVideoDeviceInfo: Dispatch>; videoDeviceList: VideoDeviceInfo[]; @@ -44,21 +46,23 @@ export const CallingContext = createContext(unde interface CallingProviderProps { children: React.ReactNode; token: string; + displayName: string; callClientOptions?: CallClientOptions; refreshTokenCallback?: (abortSignal?: AbortSignalLike) => Promise; } const CallingProviderBase = (props: CallingProviderProps & ErrorHandlingProps): JSX.Element => { - const { token, callClientOptions, refreshTokenCallback, onErrorCallback } = props; + const { token, displayName: initialDisplayName, callClientOptions, refreshTokenCallback, onErrorCallback } = props; // if there is no valid token then there is no valid userId const userIdFromToken = token ? getIdFromToken(token) : ''; - const [callClient, setCallClient] = useState(new CallClient({})); + const [callClient, setCallClient] = useState(new CallClient(callClientOptions)); const [callAgent, setCallAgent] = useState(undefined); const [deviceManager, setDeviceManager] = useState(undefined); const [userId, setUserId] = useState(userIdFromToken); - const [audioDevicePermission, setAudioDevicePermission] = useState('Unknown'); - const [videoDevicePermission, setVideoDevicePermission] = useState('Unknown'); + const [displayName, setDisplayName] = useState(initialDisplayName); + const [audioDevicePermission, setAudioDevicePermission] = useState('Unknown'); + const [videoDevicePermission, setVideoDevicePermission] = useState('Unknown'); const [videoDeviceInfo, setVideoDeviceInfo] = useState(undefined); const [videoDeviceList, setVideoDeviceList] = useState([]); const [audioDeviceInfo, setAudioDeviceInfo] = useState(undefined); @@ -71,18 +75,22 @@ const CallingProviderBase = (props: CallingProviderProps & ErrorHandlingProps): }, [refreshTokenCallback]); useEffect(() => { - if (!token) return; - (async () => { try { - setCallClient(new CallClient(callClientOptions)); - const callAgent = await callClient.createCallAgent( - createAzureCommunicationUserCredential(token, refreshTokenCallbackRefContainer.current) - ); - setCallAgent(callAgent); - - const deviceManager = await callClient.getDeviceManager(); - setDeviceManager(deviceManager); + // Need refactor: to support having ConfigurationScreen separate from GroupCall in Calling Sample, we need to + // allow DeviceManager to be created but not create CallAgent because ConfigurationScreen depends on + // DeviceManager but CallAgent depends on ConfigurationScreen displayName. So the CallingProvider used by + // ConfigurationScreen will pass in undefined token while all other cases like when CallingProvider is used by + // GroupCall or used in composite, you should pass in the valid token. + if (token) { + setCallAgent( + await callClient.createCallAgent( + createAzureCommunicationUserCredential(token, refreshTokenCallbackRefContainer.current), + { displayName: displayName } + ) + ); + } + setDeviceManager(await callClient.getDeviceManager()); } catch (error) { throw new CommunicationUiError({ message: 'Error creating call agent', @@ -93,16 +101,7 @@ const CallingProviderBase = (props: CallingProviderProps & ErrorHandlingProps): })().catch((error) => { propagateError(error, onErrorCallback); }); - }, [ - token, - callClient, - setCallAgent, - setDeviceManager, - callClientOptions, - setCallClient, - refreshTokenCallbackRefContainer, - onErrorCallback - ]); + }, [token, callClient, refreshTokenCallbackRefContainer, displayName, onErrorCallback]); // Clean up callAgent whenever the callAgent or userTokenCredential is changed. This is required because callAgent itself is a singleton. // We need to clean up before creating another one. @@ -129,6 +128,8 @@ const CallingProviderBase = (props: CallingProviderProps & ErrorHandlingProps): setDeviceManager, userId, setUserId, + displayName, + setDisplayName, audioDevicePermission, setAudioDevicePermission, videoDevicePermission, diff --git a/packages/communication-ui/src/providers/ChatProvider.tsx b/packages/communication-ui/src/providers/ChatProvider.tsx index ae549d19aa9..2678b0b13e9 100644 --- a/packages/communication-ui/src/providers/ChatProvider.tsx +++ b/packages/communication-ui/src/providers/ChatProvider.tsx @@ -6,7 +6,7 @@ import { ChatClient } from '@azure/communication-chat'; import { chatClientDeclaratify } from '@azure/acs-chat-declarative'; import { ChatThreadProvider } from './ChatThreadProvider'; import { AbortSignalLike } from '@azure/core-http'; -import { createAzureCommunicationUserCredential } from '../utils'; +import { createAzureCommunicationUserCredentialBeta } from '../utils'; import { Spinner } from '@fluentui/react'; import { getIdFromToken } from '../utils'; import { WithErrorHandling } from '../utils/WithErrorHandling'; @@ -49,7 +49,7 @@ const ChatProviderBase = (props: ChatProviderProps & ErrorHandlingProps): JSX.El const [displayName, setDisplayName] = useState(props.displayName); const [chatClient, setChatClient] = useState( chatClientDeclaratify( - new ChatClient(props.endpointUrl, createAzureCommunicationUserCredential(token, props.refreshTokenCallback)), + new ChatClient(props.endpointUrl, createAzureCommunicationUserCredentialBeta(token, props.refreshTokenCallback)), { userId, displayName } ) ); diff --git a/packages/communication-ui/src/providers/IncomingCallsProvider.tsx b/packages/communication-ui/src/providers/IncomingCallsProvider.tsx index 021e10e1dad..18f7bf3ba9e 100644 --- a/packages/communication-ui/src/providers/IncomingCallsProvider.tsx +++ b/packages/communication-ui/src/providers/IncomingCallsProvider.tsx @@ -1,43 +1,48 @@ // © Microsoft Corporation. All rights reserved. -import { Call, CollectionUpdatedEvent } from '@azure/communication-calling'; -import React, { createContext, useState } from 'react'; +import { CallEndedEvent, IncomingCall, IncomingCallEvent } from '@azure/communication-calling'; +import React, { createContext, Dispatch, SetStateAction, useState } from 'react'; import { useValidContext } from '../utils'; import { useEffect } from 'react'; import { useCallingContext } from './CallingProvider'; export type IncomingCallsContextType = { - incomingCalls: Call[]; + incomingCalls: IncomingCall[]; + setIncomingCalls: Dispatch>; }; export const IncomingCallsContext = createContext(undefined); export const IncomingCallsProvider = (props: { children: React.ReactNode }): JSX.Element => { - const [incomingCalls, setIncomingCalls] = useState([]); + const [incomingCalls, setIncomingCalls] = useState([]); const { callAgent } = useCallingContext(); - // Update `incomingCalls` whenever a new call is added or removed. - // Also configures an event on each call so that it removes itself from - // active calls. + // Update `incomingCalls` whenever a new incoming call is added or removed. useEffect(() => { - const onCallsUpdate: CollectionUpdatedEvent = () => { - const validCalls = callAgent?.calls.filter((c: Call) => c.isIncoming) ?? []; - setIncomingCalls(validCalls); - validCalls.forEach((c) => { - c.on('callStateChanged', () => { - if (c.state !== 'Incoming') { - validCalls.splice(validCalls.indexOf(c), 1); - setIncomingCalls(validCalls); + const onIncomingCall: IncomingCallEvent = (incomingCallEvent: { incomingCall: IncomingCall }): void => { + setIncomingCalls([...incomingCalls, incomingCallEvent.incomingCall]); + + const onIncomingCallEnded: CallEndedEvent = () => { + const incomingCallWithEndedOneRemoved: IncomingCall[] = []; + for (const incomingCall of incomingCalls) { + if (incomingCall !== incomingCallEvent.incomingCall) { + incomingCallWithEndedOneRemoved.push(incomingCall); } - }); - }); + } + setIncomingCalls(incomingCallWithEndedOneRemoved); + }; + incomingCallEvent.incomingCall.on('callEnded', onIncomingCallEnded); }; - callAgent?.on('callsUpdated', onCallsUpdate); - return () => callAgent?.off('callsUpdated', onCallsUpdate); - }, [callAgent, setIncomingCalls]); + callAgent?.on('incomingCall', onIncomingCall); + return () => callAgent?.off('incomingCall', onIncomingCall); + }, [callAgent, incomingCalls]); - return {props.children}; + return ( + + {props.children} + + ); }; export const useIncomingCallsContext = (): IncomingCallsContextType => useValidContext(IncomingCallsContext); diff --git a/packages/communication-ui/src/types/DevicePermission.ts b/packages/communication-ui/src/types/DevicePermission.ts new file mode 100644 index 00000000000..ad2fc6a1a21 --- /dev/null +++ b/packages/communication-ui/src/types/DevicePermission.ts @@ -0,0 +1,3 @@ +// © Microsoft Corporation. All rights reserved. +export type DevicePermissionState = 'Granted' | 'Denied' | 'Unknown'; +export type DevicePermissionType = 'Camera' | 'Microphone'; diff --git a/packages/communication-ui/src/types/index.ts b/packages/communication-ui/src/types/index.ts index 32b00b53848..7d6a6e4d202 100644 --- a/packages/communication-ui/src/types/index.ts +++ b/packages/communication-ui/src/types/index.ts @@ -9,3 +9,4 @@ export * from './ParticipantStream'; export * from './TypingUser'; export * from './CommunicationUiError'; export * from './CustomStylesProps'; +export * from './DevicePermission'; diff --git a/packages/communication-ui/src/utils/SDKUtils.test.ts b/packages/communication-ui/src/utils/SDKUtils.test.ts index 03775756058..5d673ba97e4 100644 --- a/packages/communication-ui/src/utils/SDKUtils.test.ts +++ b/packages/communication-ui/src/utils/SDKUtils.test.ts @@ -1,7 +1,7 @@ // © Microsoft Corporation. All rights reserved. import { AudioDeviceInfo } from '@azure/communication-calling'; -import { CallingApplication, CommunicationUser, PhoneNumber, UnknownIdentifier } from '@azure/communication-common'; +import { CommunicationUserKind, PhoneNumberKind, UnknownIdentifierKind } from '@azure/communication-common'; import { getACSId, isGUID, isInCall, isSelectedDeviceInList } from './SDKUtils'; describe('SDKUtils tests', () => { @@ -10,36 +10,10 @@ describe('SDKUtils tests', () => { jest.resetAllMocks(); }); - jest.mock('@azure/communication-calling', () => { - return { - isCommunicationUser: (identifer: CommunicationUser | CallingApplication | UnknownIdentifier | PhoneNumber) => { - return 'communicationUserId' in identifer; - }, - isCallingApplication: (identifer: CommunicationUser | CallingApplication | UnknownIdentifier | PhoneNumber) => { - return 'callingApplicationId' in identifer; - }, - isPhoneNumber: (identifer: CommunicationUser | CallingApplication | UnknownIdentifier | PhoneNumber) => { - return 'phoneNumber' in identifer; - } - }; - }); - test('getACSId should return CommunicationUserId if the identifier is a CommunicationUser', () => { // Arrange const expectedId = 'testUserId'; - const identifer: CommunicationUser = { communicationUserId: expectedId }; - - // Act - const id = getACSId(identifer); - - // Assert - expect(id).toEqual(expectedId); - }); - - test('getACSId should return CallingApplicationId if the identifier is a CallingApplication', () => { - // Arrange - const expectedId = 'testCallApplicationId'; - const identifer: CallingApplication = { callingApplicationId: expectedId }; + const identifer: CommunicationUserKind = { communicationUserId: expectedId, kind: 'communicationUser' }; // Act const id = getACSId(identifer); @@ -51,7 +25,7 @@ describe('SDKUtils tests', () => { test('getACSId should return PhoneNumber if the identifier is a PhoneNumber', () => { // Arrange const expectedId = 'testPhoneNumber'; - const identifer: PhoneNumber = { phoneNumber: expectedId }; + const identifer: PhoneNumberKind = { phoneNumber: expectedId, kind: 'phoneNumber' }; // Act const id = getACSId(identifer); @@ -60,10 +34,10 @@ describe('SDKUtils tests', () => { expect(id).toEqual(expectedId); }); - test('getACSId should return id if the identifier is a something other than CommunicationUser, CallingApplication or PhoneNumber', () => { + test('getACSId should return id if the identifier is unknown', () => { // Arrange const expectedId = 'testId'; - const identifer: UnknownIdentifier = { id: expectedId }; + const identifer: UnknownIdentifierKind = { id: expectedId, kind: 'unknown' }; // Act const id = getACSId(identifer); @@ -110,11 +84,11 @@ describe('SDKUtils tests', () => { expect(isInCall('Disconnected')).toEqual(false); // true conditions - expect(isInCall('Incoming')).toEqual(true); expect(isInCall('Connecting')).toEqual(true); expect(isInCall('Ringing')).toEqual(true); expect(isInCall('Connected')).toEqual(true); - expect(isInCall('Hold')).toEqual(true); + expect(isInCall('LocalHold')).toEqual(true); + expect(isInCall('RemoteHold')).toEqual(true); expect(isInCall('InLobby')).toEqual(true); expect(isInCall('Disconnecting')).toEqual(true); expect(isInCall('EarlyMedia')).toEqual(true); diff --git a/packages/communication-ui/src/utils/SDKUtils.ts b/packages/communication-ui/src/utils/SDKUtils.ts index a9c25f39841..94bb4e5e592 100644 --- a/packages/communication-ui/src/utils/SDKUtils.ts +++ b/packages/communication-ui/src/utils/SDKUtils.ts @@ -1,17 +1,17 @@ // © Microsoft Corporation. All rights reserved. -import { AzureCommunicationUserCredential, RefreshOptions } from '@azure/communication-common'; - -import { AudioDeviceInfo, CallState, LocalVideoStream, VideoDeviceInfo } from '@azure/communication-calling'; import { - CallingApplication, - CommunicationUser, - PhoneNumber, - UnknownIdentifier, - isCallingApplication, - isCommunicationUser, - isPhoneNumber + AzureCommunicationTokenCredential, + CommunicationUserKind, + MicrosoftTeamsUserKind, + PhoneNumberKind, + CommunicationTokenRefreshOptions, + UnknownIdentifierKind } from '@azure/communication-common'; + +import { AzureCommunicationUserCredential, RefreshOptions } from '@azure/communication-common-beta3'; + +import { AudioDeviceInfo, CallState, LocalVideoStream, VideoDeviceInfo } from '@azure/communication-calling'; import { CommunicationUiErrorCode, CommunicationUiError, @@ -32,16 +32,21 @@ import { } from '../constants/chatConstants'; export const getACSId = ( - identifier: CommunicationUser | CallingApplication | UnknownIdentifier | PhoneNumber + identifier: CommunicationUserKind | PhoneNumberKind | MicrosoftTeamsUserKind | UnknownIdentifierKind ): string => { - if (isCommunicationUser(identifier)) { - return identifier.communicationUserId; - } else if (isCallingApplication(identifier)) { - return identifier.callingApplicationId; - } else if (isPhoneNumber(identifier)) { - return identifier.phoneNumber; - } else { - return identifier.id; + switch (identifier.kind) { + case 'communicationUser': { + return identifier.communicationUserId; + } + case 'phoneNumber': { + return identifier.phoneNumber; + } + case 'microsoftTeamsUser': { + return identifier.microsoftTeamsUserId; + } + default: { + return identifier.id; + } } }; @@ -60,7 +65,7 @@ export const isGUID = (str: string): boolean => !!str.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i); export const areStreamsEqual = (prevStream: LocalVideoStream, newStream: LocalVideoStream): boolean => { - return !!prevStream && !!newStream && prevStream.getSource().id === newStream.getSource().id; + return !!prevStream && !!newStream && prevStream.source.id === newStream.source.id; }; export const isMobileSession = (): boolean => @@ -68,7 +73,8 @@ export const isMobileSession = (): boolean => // Create AzureCommunicationUserCredential using optional refreshTokenCallback if provided. If callback is provided then // identity must also be provided for callback to be used. -export const createAzureCommunicationUserCredential = ( +// TODO: Delete this and use the one below once Chat has been upgraded to latest common +export const createAzureCommunicationUserCredentialBeta = ( token: string, refreshTokenCallback?: (() => Promise) | undefined ): AzureCommunicationUserCredential => { @@ -84,6 +90,24 @@ export const createAzureCommunicationUserCredential = ( } }; +// Create AzureCommunicationUserCredential using optional refreshTokenCallback if provided. If callback is provided then +// identity must also be provided for callback to be used. +export const createAzureCommunicationUserCredential = ( + token: string, + refreshTokenCallback?: (() => Promise) | undefined +): AzureCommunicationTokenCredential => { + if (refreshTokenCallback !== undefined) { + const options: CommunicationTokenRefreshOptions = { + token: token, + tokenRefresher: () => refreshTokenCallback(), + refreshProactively: true + }; + return new AzureCommunicationTokenCredential(options); + } else { + return new AzureCommunicationTokenCredential(token); + } +}; + // This is a temporary solution to grab the userId from the token. // In the future we want to use the AzureCommunicationUserCredential and CommunicationUser types into the Provider export const getIdFromToken = (jwtToken: string): string => { diff --git a/packages/communication-ui/src/utils/TypeConverter.ts b/packages/communication-ui/src/utils/TypeConverter.ts index 132f7c305c7..68231106e80 100644 --- a/packages/communication-ui/src/utils/TypeConverter.ts +++ b/packages/communication-ui/src/utils/TypeConverter.ts @@ -21,7 +21,9 @@ export const convertSdkRemoteParticipantToListParticipant = ( onRemove?: () => void, onMute?: () => void ): ListParticipant => { - const isScreenSharing = participant.videoStreams.some((vs) => vs.type === 'ScreenSharing' && vs.isAvailable); + const isScreenSharing = participant.videoStreams.some( + (vs) => vs.mediaStreamType === 'ScreenSharing' && vs.isAvailable + ); const identifier = getACSId(participant.identifier); return { key: identifier, diff --git a/packages/storybook/package.json b/packages/storybook/package.json index 3d9862e2eff..ed5df899704 100644 --- a/packages/storybook/package.json +++ b/packages/storybook/package.json @@ -19,8 +19,8 @@ }, "dependencies": { "@azure/communication-administration": "1.0.0-beta.3", - "@azure/communication-calling": "1.0.0-beta.2", - "@azure/communication-chat": "1.0.0-beta.3", + "@azure/communication-calling": "1.0.1-beta.1", + "@azure/communication-chat": "1.0.0-beta.4", "@azure/communication-common": "1.0.0-beta.3", "@azure/communication-signaling": "1.0.0-beta.1", "@azure/communication-ui": "1.0.0-beta", diff --git a/packages/storybook/stories/GroupCallComposite/GroupCallCompositeDocs.tsx b/packages/storybook/stories/GroupCallComposite/GroupCallCompositeDocs.tsx index f7ff2511b86..f8badf80f88 100644 --- a/packages/storybook/stories/GroupCallComposite/GroupCallCompositeDocs.tsx +++ b/packages/storybook/stories/GroupCallComposite/GroupCallCompositeDocs.tsx @@ -7,7 +7,6 @@ import { GroupCall } from '@azure/communication-ui'; const importStatement = ` import { GroupCall } from '@azure/communication-ui'; import { v1 as createGUID } from 'uuid'; -import { AzureCommunicationUserCredential } from '@azure/communication-common'; import { CommunicationIdentityClient, CommunicationUserToken } from '@azure/communication-administration'; `; diff --git a/packages/storybook/stories/__snapshots__/storybook.test.ts.snap b/packages/storybook/stories/__snapshots__/storybook.test.ts.snap index 7cb469d4eb6..6126b513bd4 100644 --- a/packages/storybook/stories/__snapshots__/storybook.test.ts.snap +++ b/packages/storybook/stories/__snapshots__/storybook.test.ts.snap @@ -39,6 +39,45 @@ exports[`storybook snapshot tests Storyshots Composites/GroupCall Group Call Com `; +exports[`storybook snapshot tests Storyshots Composites/GroupChat Group Chat Composite 1`] = ` +
+
+
+
+ Please fill in Connection String or required params to run GroupChat. +
+
+
+
+`; + exports[`storybook snapshot tests Storyshots Composites/OneToOneCall One To One Call Composite Component 1`] = `
{ const [page, setPage] = useState('home'); - const [subpage, setSubpage] = useState('configuration'); const [groupId, setGroupId] = useState(''); const [screenWidth, setScreenWidth] = useState(window?.innerWidth ?? 0); const [token, setToken] = useState(''); const [userId, setUserId] = useState(''); + const [displayName, setDisplayName] = useState(''); useEffect(() => { const setWindowWidth = (): void => { @@ -80,6 +80,29 @@ const App = (): JSX.Element => { /> ); } + case 'configuration': { + return ( + + console.error('onErrorCallback received error:', error) + } + > + + + setPage('call')} + onDisplayNameUpdate={setDisplayName} + /> + + + + ); + } case 'call': { return ( { console.error('onErrorCallback received error:', error) } > - - - {(() => { - switch (subpage) { - case 'configuration': { - return ( - setSubpage('groupcall')} - groupId={getGroupId()} - /> - ); - } - case 'groupcall': { - return ( - { - setPage('endCall'); - setSubpage('configuration'); - }} - screenWidth={screenWidth} - groupId={getGroupId()} - /> - ); - } - default: { - return <>Please set a valid subpage; - } - } - })()} + + + { + setPage('endCall'); + }} + screenWidth={screenWidth} + groupId={getGroupId()} + /> @@ -126,8 +132,7 @@ const App = (): JSX.Element => { return ( { - setSubpage('configuration'); - setPage('call'); + setPage('configuration'); }} homeHandler={(): void => { window.location.href = window.location.href.split('?')[0]; @@ -153,8 +158,7 @@ const App = (): JSX.Element => { }; if (getGroupIdFromUrl() && page === 'home') { - setSubpage('configuration'); - setPage('call'); + setPage('configuration'); } if (isMobileSession() || isSmallScreen()) { diff --git a/samples/Calling/src/app/CallConfiguration.tsx b/samples/Calling/src/app/CallConfiguration.tsx index 579b51f0ab3..b2f9b347e2d 100644 --- a/samples/Calling/src/app/CallConfiguration.tsx +++ b/samples/Calling/src/app/CallConfiguration.tsx @@ -1,5 +1,5 @@ // © Microsoft Corporation. All rights reserved. -import { Spinner, Stack } from '@fluentui/react'; +import { Stack } from '@fluentui/react'; import React from 'react'; import { configurationStackTokens, @@ -17,28 +17,22 @@ export interface CallConfigurationProps extends SetupContainerProps { children: React.ReactNode; } -const spinnerLabel = 'Initializing call client...'; - export const CallConfiguration = (props: CallConfigurationProps): JSX.Element => { - const { isCallInitialized, screenWidth } = props; + const { screenWidth } = props; return ( - {isCallInitialized ? ( - 750 ? fullScreenStyle : verticalStackStyle} - horizontal={screenWidth > 750} - horizontalAlign="center" - verticalAlign="center" - tokens={screenWidth > 750 ? configurationStackTokens : undefined} - grow - > - - {props.children} - - ) : ( - - )} + 750 ? fullScreenStyle : verticalStackStyle} + horizontal={screenWidth > 750} + horizontalAlign="center" + verticalAlign="center" + tokens={screenWidth > 750 ? configurationStackTokens : undefined} + grow + > + + {props.children} + ); }; diff --git a/samples/Calling/src/app/ConfigurationScreen.tsx b/samples/Calling/src/app/ConfigurationScreen.tsx index f103c215240..865060a49ae 100644 --- a/samples/Calling/src/app/ConfigurationScreen.tsx +++ b/samples/Calling/src/app/ConfigurationScreen.tsx @@ -12,18 +12,18 @@ import { LocalDeviceSettings } from './LocalDeviceSettings'; export interface ConfigurationScreenProps extends SetupContainerProps { screenWidth: number; startCallHandler(): void; - groupId: string; + onDisplayNameUpdate: (displayName: string) => void; } export const ConfigurationComponent = (props: ConfigurationScreenProps): JSX.Element => { - const { displayName, updateDisplayName, startCallHandler, joinCall, groupId } = props; + const { displayName, startCallHandler, onDisplayNameUpdate } = props; const [emptyWarning, setEmptyWarning] = useState(false); const [nameTooLongWarning, setNameTooLongWarning] = useState(false); return (
{ + onClickHandler={async () => { if (localStorageAvailable) { saveDisplayNameToLocalStorage(displayName); } startCallHandler(); - joinCall(groupId); }} isDisabled={!displayName || emptyWarning || nameTooLongWarning} /> diff --git a/samples/Calling/src/app/GroupCall.tsx b/samples/Calling/src/app/GroupCall.tsx index 2802902da80..4e990f9a23b 100644 --- a/samples/Calling/src/app/GroupCall.tsx +++ b/samples/Calling/src/app/GroupCall.tsx @@ -33,18 +33,36 @@ const spinnerLabel = 'Initializing call client...'; const GroupCallComponent = (props: GroupCallProps): JSX.Element => { const [selectedPane, setSelectedPane] = useState(CommandPanelTypes.None); - const { isCallInitialized, callState, isLocalScreenSharingOn, groupId, screenWidth, endCallHandler } = props; + const { + callAgentSubscribed, + isCallInitialized, + callState, + isLocalScreenSharingOn, + groupId, + screenWidth, + endCallHandler, + joinCall + } = props; const ErrorBar = connectFuncsToContext(ErrorBarComponent, MapToErrorBarProps); + const [joinCallCalled, setJoinCallCalled] = useState(false); useEffect(() => { if (isInCall(callState)) { document.title = `${groupId} group call sample`; + } else { + if (callAgentSubscribed && !joinCallCalled) { + // Need refactor: We might not have joined call yet if there is no Call object, See comment in CallingProvider + // for more details and useCallAgent + joinCall(groupId); + setJoinCallCalled(true); + } } - }, [callState, groupId]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [callState, groupId, callAgentSubscribed]); return ( <> - {isCallInitialized ? ( + {isCallInitialized && isInCall(callState) ? (
Promise; + joinCall: (groupId: string) => void; + leaveCall: (hangupCallOptions: HangUpOptions) => Promise; }; export const MapToGroupCallProps = (): GroupCallContainerProps => { const { callAgent, deviceManager } = useCallingContext(); const { call, localScreenShareActive } = useCallContext(); - const { leave } = useGroupCall(); + const { join, leave } = useGroupCall(); // Call useCallAgent to subscribe to events. - useCallAgent(); + const subscribed = useCallAgent(); return { + callAgentSubscribed: subscribed, isCallInitialized: !!(callAgent && deviceManager), callState: call?.state ?? 'None', isLocalScreenSharingOn: localScreenShareActive, - leaveCall: async (hangupCallOptions: HangupCallOptions) => { + joinCall: (groupId: string) => { + !call && join({ groupId: groupId }); + }, + leaveCall: async (hangupCallOptions: HangUpOptions) => { await leave(hangupCallOptions); } }; diff --git a/samples/Calling/src/app/consumers/MapToMediaControlsProps.ts b/samples/Calling/src/app/consumers/MapToMediaControlsProps.ts index 966a61b45a1..96d91bcb92d 100644 --- a/samples/Calling/src/app/consumers/MapToMediaControlsProps.ts +++ b/samples/Calling/src/app/consumers/MapToMediaControlsProps.ts @@ -1,6 +1,6 @@ // © Microsoft Corporation. All rights reserved. -import { HangupCallOptions, PermissionState as DevicePermissionState } from '@azure/communication-calling'; +import { HangUpOptions } from '@azure/communication-calling'; import { useCallContext, useCallingContext, @@ -12,7 +12,8 @@ import { isMobileSession, useGroupCall, CommunicationUiErrorCode, - CommunicationUiError + CommunicationUiError, + DevicePermissionState } from '@azure/communication-ui'; import { useCallback } from 'react'; @@ -52,7 +53,7 @@ export type MediaControlsContainerProps = { /** Determines if screen share is supported by browser. */ isLocalScreenShareSupportedInBrowser(): boolean; /** Callback when leaving the call. */ - leaveCall: (hangupCallOptions: HangupCallOptions) => Promise; + leaveCall: (hangupCallOptions: HangUpOptions) => Promise; }; export const MapToMediaControlsProps = (): MediaControlsContainerProps => { @@ -114,7 +115,7 @@ export const MapToMediaControlsProps = (): MediaControlsContainerProps => { cameraPermission: videoDevicePermission, micPermission: audioDevicePermission, isLocalScreenShareSupportedInBrowser, - leaveCall: async (hangupCallOptions: HangupCallOptions): Promise => { + leaveCall: async (hangupCallOptions: HangUpOptions): Promise => { await leave(hangupCallOptions); } }; diff --git a/samples/Calling/src/app/consumers/MapToMediaGalleryProps.ts b/samples/Calling/src/app/consumers/MapToMediaGalleryProps.ts index 1ab36773f43..a8e566de8c7 100644 --- a/samples/Calling/src/app/consumers/MapToMediaGalleryProps.ts +++ b/samples/Calling/src/app/consumers/MapToMediaGalleryProps.ts @@ -20,8 +20,8 @@ export type MediaGalleryContainerProps = { }; export const MapToMediaGalleryProps = (): MediaGalleryContainerProps => { - const { participants, displayName, screenShareStream, localVideoStream } = useCallContext(); - const { userId } = useCallingContext(); + const { participants, screenShareStream, localVideoStream } = useCallContext(); + const { userId, displayName } = useCallingContext(); const [remoteParticipants, setRemoteParticipants] = useState([]); const [localParticipant, setLocalParticipant] = useState({ userId, diff --git a/samples/Calling/src/app/consumers/MapToParticipantListProps.ts b/samples/Calling/src/app/consumers/MapToParticipantListProps.ts index 61d64addb55..b13e3499f9d 100644 --- a/samples/Calling/src/app/consumers/MapToParticipantListProps.ts +++ b/samples/Calling/src/app/consumers/MapToParticipantListProps.ts @@ -16,8 +16,8 @@ type ParticipantListContainerProps = { }; export const MapToParticipantListProps = (): ParticipantListContainerProps => { - const { userId } = useCallingContext(); - const { call, participants, displayName } = useCallContext(); + const { userId, displayName } = useCallingContext(); + const { call, participants } = useCallContext(); const remoteParticipants = participants.map((p) => convertSdkRemoteParticipantToListParticipant(p, call ? () => call.removeParticipant(p.identifier) : undefined) @@ -28,6 +28,6 @@ export const MapToParticipantListProps = (): ParticipantListContainerProps => { isScreenSharingOn: call?.isScreenSharingOn ?? false, displayName: displayName, userId: userId, - isMuted: call?.isMicrophoneMuted ?? false + isMuted: call?.isMuted ?? false }; }; diff --git a/samples/OneToOneCall/package.json b/samples/OneToOneCall/package.json index 909009509a9..42fb97b0446 100644 --- a/samples/OneToOneCall/package.json +++ b/samples/OneToOneCall/package.json @@ -50,8 +50,8 @@ }, "dependencies": { "@azure/communication-administration": "1.0.0-beta.3", - "@azure/communication-calling": "1.0.0-beta.2", - "@azure/communication-common": "1.0.0-beta.3", + "@azure/communication-calling": "1.0.1-beta.1", + "@azure/communication-common": "1.0.0", "@azure/communication-ui": "1.0.0-beta", "@azure/core-http": "^1.2.3", "@babel/preset-react": "^7.12.7",