diff --git a/change/@azure-communication-react-14569316-6850-4cc1-8ce1-326ec40aefdb.json b/change/@azure-communication-react-14569316-6850-4cc1-8ce1-326ec40aefdb.json new file mode 100644 index 00000000000..55d2a7eefc2 --- /dev/null +++ b/change/@azure-communication-react-14569316-6850-4cc1-8ce1-326ec40aefdb.json @@ -0,0 +1,8 @@ +{ + "type": "prerelease", + "area": "feature", + "workstream": "togetherMode", + "comment": "Implemented support logic for call feature streams. This functionality will facilitate the creation of call feature streams that are independent of any participant in the call or the device manager", + "packageName": "@azure/communication-react", + "dependentChangeType": "patch" +} diff --git a/change/@azure-communication-react-206ba832-b149-4702-9fbe-2995f7b92f36.json b/change/@azure-communication-react-206ba832-b149-4702-9fbe-2995f7b92f36.json new file mode 100644 index 00000000000..c1c737b1635 --- /dev/null +++ b/change/@azure-communication-react-206ba832-b149-4702-9fbe-2995f7b92f36.json @@ -0,0 +1,8 @@ +{ + "type": "prerelease", + "area": "feature", + "workstream": "togetherMode", + "comment": "Addressed comments by moving createCallFeatureView and disposeCallFeatureView to its own file", + "packageName": "@azure/communication-react", + "dependentChangeType": "patch" +} diff --git a/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts b/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts index f84e2f950ab..c1fd91f4efb 100644 --- a/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts +++ b/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts @@ -40,6 +40,8 @@ import { Features } from '@azure/communication-calling'; import { TeamsCaptions } from '@azure/communication-calling'; import { Reaction } from '@azure/communication-calling'; import { _ComponentCallingHandlers } from './createHandlers'; +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeStreamViewResult } from '@internal/react-components/dist/dist-esm/types/TogetherModeTypes'; /** * Object containing all the handlers required for calling components. @@ -103,6 +105,20 @@ export interface CommonCallingHandlers { onStopAllSpotlight: () => Promise; onMuteParticipant: (userId: string) => Promise; onMuteAllRemoteParticipants: () => Promise; + /* @conditional-compile-remove(together-mode) */ + /** + * Call back to create a view for together mode + * + * @beta + */ + onCreateTogetherModeStreamView: (options?: VideoStreamOptions) => Promise; + /* @conditional-compile-remove(together-mode) */ + /** + * Call back to dispose together mode views + * + * @beta + */ + onDisposeTogetherModeStreamViews: () => Promise; } /** @@ -712,6 +728,54 @@ export const createDefaultCommonCallingHandlers = memoizeOne( } : undefined; + /* @conditional-compile-remove(together-mode) */ + const onCreateTogetherModeStreamView = async ( + options = { scalingMode: 'Fit', isMirrored: true } as VideoStreamOptions + ): Promise => { + if (!call) { + return; + } + const callState = callClient.getState().calls[call.id]; + if (!callState) { + return; + } + const togetherModeStreams = callState.togetherMode.streams; + const togetherModeCreateViewResult: TogetherModeStreamViewResult = {}; + if (!togetherModeStreams.mainVideoStream) { + const togetherModeFeature = call?.feature(Features.TogetherMode); + await togetherModeFeature?.start(); + } else { + const mainVideoStream = togetherModeStreams.mainVideoStream; + if (mainVideoStream && !mainVideoStream.view) { + const createViewResult = await callClient.createCallFeatureView(call.id, mainVideoStream, options); + // SDK currently only supports 1 Video media stream type + togetherModeCreateViewResult.mainVideoView = createViewResult?.view + ? { view: createViewResult?.view } + : undefined; + } + } + return togetherModeCreateViewResult; + }; + /* @conditional-compile-remove(together-mode) */ + const onDisposeTogetherModeStreamViews = async (): Promise => { + if (!call) { + return; + } + const callState = callClient.getState().calls[call.id]; + if (!callState) { + throw new Error(`Call Not Found: ${call.id}`); + } + + const togetherModeStreams = callState.togetherMode.streams; + + if (!togetherModeStreams.mainVideoStream) { + return; + } + + if (togetherModeStreams.mainVideoStream.view) { + callClient.disposeCallFeatureView(call.id, togetherModeStreams.mainVideoStream); + } + }; return { onHangUp, onToggleHold, @@ -761,7 +825,11 @@ export const createDefaultCommonCallingHandlers = memoizeOne( onMuteParticipant, onMuteAllRemoteParticipants, onAcceptCall: notImplemented, - onRejectCall: notImplemented + onRejectCall: notImplemented, + /* @conditional-compile-remove(together-mode) */ + onCreateTogetherModeStreamView, + /* @conditional-compile-remove(together-mode) */ + onDisposeTogetherModeStreamViews }; } ); diff --git a/packages/calling-stateful-client/src/CallClientState.ts b/packages/calling-stateful-client/src/CallClientState.ts index ff602e96432..47452b86f5f 100644 --- a/packages/calling-stateful-client/src/CallClientState.ts +++ b/packages/calling-stateful-client/src/CallClientState.ts @@ -272,45 +272,71 @@ export interface RaiseHandCallFeatureState { /* @conditional-compile-remove(together-mode) */ /** - * State only version of {@link @azure/communication-calling#TogetherModeCallFeature}. {@link StatefulCallClient} will - * automatically listen for raised hands on the call and update the state exposed by {@link StatefulCallClient} accordingly. - * @alpha + * @beta */ -export interface TogetherModeCallFeatureState { - /** - * Proxy of {@link @azure/communication-calling#TogetherModeCallFeature.togetherModeStream}. - */ - stream: TogetherModeStreamState[]; +export type CallFeatureStreamName = 'togetherMode'; + +/* @conditional-compile-remove(together-mode) */ +/** + * @beta + */ +export interface CallFeatureStreamState { + feature?: CallFeatureStreamName; } /* @conditional-compile-remove(together-mode) */ /** * State only version of {@link @azure/communication-calling#TogetherModeVideoStream}. - * @alpha + * @beta */ -export interface TogetherModeStreamState { - /** - * Proxy of {@link @azure/communication-calling#TogetherModeVideoStream.id}. - */ - id: number; - /** - * Proxy of {@link @azure/communication-calling#TogetherModeVideoStream.mediaStreamType}. - */ - mediaStreamType: MediaStreamType; - /** - * Proxy of {@link @azure/communication-calling#TogetherModeVideoStream.isReceiving}. - * @public - */ - isReceiving: boolean; +export interface TogetherModeStreamViewState extends RemoteVideoStreamState, CallFeatureStreamState {} + +/* @conditional-compile-remove(together-mode) */ +/** + * State only version of {@link @azure/communication-calling#TogetherModeSeatingMap}. + * @beta + * + * Represents the seating position of a participant in Together Mode. + */ +export interface TogetherModeSeatingPositionState { + // The participant id of the participant in the seating position. + participantId: string; + // The top left offset from the top of the together mode view. + top: number; + // The left offset position from the left of the together mode view. + left: number; + // The width of the seating area + width: number; + // The height of the seating area. + height: number; +} + +/* @conditional-compile-remove(together-mode) */ +/** + * Interface representing the streams in Together Mode. + * + * @beta + */ +export interface TogetherModeStreamsState { + mainVideoStream?: TogetherModeStreamViewState; +} + +/* @conditional-compile-remove(together-mode) */ +/** + * State only version of {@link @azure/communication-calling#TogetherModeCallFeature}. {@link StatefulCallClient} will + * automatically listen for raised hands on the call and update the state exposed by {@link StatefulCallClient} accordingly. + * @beta + */ +export interface TogetherModeCallFeatureState { + isActive: boolean; /** - * {@link VideoStreamRendererView} that is managed by createView/disposeView in {@link StatefulCallClient} - * API. This can be undefined if the stream has not yet been rendered and defined after createView creates the view. + * Proxy of {@link @azure/communication-calling#TogetherModeCallFeature.togetherModeStream}. */ - view?: VideoStreamRendererViewState; + streams: TogetherModeStreamsState; /** - * Proxy of {@link @azure/communication-calling#RemoteVideoStream.size}. + * Proxy of {@link @azure/communication-calling#TogetherModeCallFeature.TogetherModeSeatingMap}. */ - streamSize?: { width: number; height: number }; + seatingPositions: TogetherModeSeatingPositionState[]; } /** @@ -621,6 +647,7 @@ export interface CallState { /* @conditional-compile-remove(together-mode) */ /** * Proxy of {@link @azure/communication-calling#TogetherModeCallFeature}. + * @beta */ togetherMode: TogetherModeCallFeatureState; /** diff --git a/packages/calling-stateful-client/src/CallContext.ts b/packages/calling-stateful-client/src/CallContext.ts index 0f29af497e5..820e91a33b2 100644 --- a/packages/calling-stateful-client/src/CallContext.ts +++ b/packages/calling-stateful-client/src/CallContext.ts @@ -23,7 +23,7 @@ import { TeamsCaptionsInfo } from '@azure/communication-calling'; import { CaptionsKind, CaptionsInfo as AcsCaptionsInfo } from '@azure/communication-calling'; import { EnvironmentInfo } from '@azure/communication-calling'; /* @conditional-compile-remove(together-mode) */ -import { TogetherModeVideoStream } from '@azure/communication-calling'; +import { TogetherModeVideoStream, TogetherModeSeatingMap } from '@azure/communication-calling'; import { AzureLogger, createClientLogger, getLogLevel } from '@azure/logger'; import { EventEmitter } from 'events'; import { enableMapSet, enablePatches, Patch, produce } from 'immer'; @@ -65,6 +65,8 @@ import { SpotlightedParticipant } from '@azure/communication-calling'; import { LocalRecordingInfo } from '@azure/communication-calling'; /* @conditional-compile-remove(local-recording-notification) */ import { RecordingInfo } from '@azure/communication-calling'; +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeStreamViewState, TogetherModeSeatingPositionState } from './CallClientState'; enableMapSet(); // Needed to generate state diff for verbose logging. @@ -455,11 +457,78 @@ export class CallContext { } /* @conditional-compile-remove(together-mode) */ - public setTogetherModeVideoStream(callId: string, addedStream: TogetherModeVideoStream[]): void { + public setTogetherModeVideoStreams( + callId: string, + addedStreams: TogetherModeStreamViewState[], + removedStreams: TogetherModeStreamViewState[] + ): void { + this.modifyState((draft: CallClientState) => { + const call = draft.calls[this._callIdHistory.latestCallId(callId)]; + if (call) { + for (const stream of removedStreams) { + if (stream.mediaStreamType === 'Video') { + call.togetherMode.streams.mainVideoStream = undefined; + call.togetherMode.isActive = false; + call.togetherMode.seatingPositions = []; + } + } + + for (const newStream of addedStreams) { + // This should only be called by the subscriber and some properties are add by other components so if the + // stream already exists, only update the values that subscriber knows about. + const mainVideoStream = call.togetherMode.streams.mainVideoStream; + if (mainVideoStream && mainVideoStream.id === newStream.id) { + mainVideoStream.mediaStreamType = newStream.mediaStreamType; + mainVideoStream.isAvailable = newStream.isAvailable; + mainVideoStream.isReceiving = newStream.isReceiving; + } else { + call.togetherMode.streams.mainVideoStream = newStream; + } + call.togetherMode.isActive = true; + } + } + }); + } + + /* @conditional-compile-remove(together-mode) */ + public setTogetherModeVideoStreamIsAvailable(callId: string, streamId: number, isAvailable: boolean): void { + this.modifyState((draft: CallClientState) => { + const call = draft.calls[this._callIdHistory.latestCallId(callId)]; + if (call) { + const stream = call.togetherMode.streams.mainVideoStream; + if (stream && stream?.id === streamId) { + stream.isReceiving = isAvailable; + } + } + }); + } + + /* @conditional-compile-remove(together-mode) */ + public setTogetherModeVideoStreamIsReceiving(callId: string, streamId: number, isReceiving: boolean): void { + this.modifyState((draft: CallClientState) => { + const call = draft.calls[this._callIdHistory.latestCallId(callId)]; + if (call) { + const stream = call.togetherMode.streams.mainVideoStream; + if (stream && stream?.id === streamId) { + stream.isReceiving = isReceiving; + } + } + }); + } + + /* @conditional-compile-remove(together-mode) */ + public setTogetherModeVideoStreamSize( + callId: string, + streamId: number, + size: { width: number; height: number } + ): void { this.modifyState((draft: CallClientState) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { - call.togetherMode = { stream: addedStream }; + const stream = call.togetherMode.streams.mainVideoStream; + if (stream && stream?.id === streamId) { + stream.streamSize = size; + } } }); } @@ -470,15 +539,37 @@ export class CallContext { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { for (const stream of removedStream) { - if (stream.mediaStreamType in call.togetherMode.stream) { - // Temporary lint fix: Remove the stream from the list - call.togetherMode.stream = []; + if (stream.mediaStreamType === 'Video') { + call.togetherMode.streams.mainVideoStream = undefined; + call.togetherMode.isActive = false; } } } }); } + /* @conditional-compile-remove(together-mode) */ + public setTogetherModeSeatingCoordinates(callId: string, seatingMap: TogetherModeSeatingMap): void { + this.modifyState((draft: CallClientState) => { + const call = draft.calls[this._callIdHistory.latestCallId(callId)]; + if (call) { + const seatingPositions: TogetherModeSeatingPositionState[] = []; + for (const [key, value] of seatingMap.entries()) { + const participantPosition: TogetherModeSeatingPositionState = { + participantId: key, + top: value.top, + left: value.left, + width: value.width, + height: value.height + }; + + seatingPositions.push(participantPosition); + } + call.togetherMode.seatingPositions = seatingPositions; + } + }); + } + public setCallRaisedHands(callId: string, raisedHands: RaisedHand[]): void { this.modifyState((draft: CallClientState) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; @@ -718,6 +809,25 @@ export class CallContext { }); } + /* @conditional-compile-remove(together-mode) */ + public setTogetherModeVideoStreamRendererView( + callId: string, + togetherModeStreamType: string, + view: VideoStreamRendererViewState | undefined + ): void { + this.modifyState((draft: CallClientState) => { + const call = draft.calls[this._callIdHistory.latestCallId(callId)]; + if (call) { + if (togetherModeStreamType === 'Video') { + const togetherModeStream = call.togetherMode.streams.mainVideoStream; + if (togetherModeStream) { + togetherModeStream.view = view; + } + } + } + }); + } + public setParticipantState(callId: string, participantKey: string, state: RemoteParticipantStatus): void { this.modifyState((draft: CallClientState) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; diff --git a/packages/calling-stateful-client/src/CallFeatureStreamUtils.ts b/packages/calling-stateful-client/src/CallFeatureStreamUtils.ts new file mode 100644 index 00000000000..52392cfbdbc --- /dev/null +++ b/packages/calling-stateful-client/src/CallFeatureStreamUtils.ts @@ -0,0 +1,293 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +/* @conditional-compile-remove(together-mode) */ +import { CreateViewOptions, VideoStreamRenderer } from '@azure/communication-calling'; +/* @conditional-compile-remove(together-mode) */ +import { CallContext } from './CallContext'; +/* @conditional-compile-remove(together-mode) */ +import { CallFeatureStreamState, CreateViewResult } from './index-public'; +/* @conditional-compile-remove(together-mode) */ +import { InternalCallContext } from './InternalCallContext'; +/* @conditional-compile-remove(together-mode) */ +import { _logStreamEvent } from './StreamUtilsLogging'; +/* @conditional-compile-remove(together-mode) */ +import { EventNames } from './Logger'; +/* @conditional-compile-remove(together-mode) */ +import { convertFromSDKToDeclarativeVideoStreamRendererView } from './Converter'; +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeStreamViewState } from './CallClientState'; + +/* @conditional-compile-remove(together-mode) */ +/** + * @private + * + */ +export function createCallFeatureView( + context: CallContext, + internalContext: InternalCallContext, + callId: string | undefined, + stream: /* @conditional-compile-remove(together-mode) */ TogetherModeStreamViewState, + options?: CreateViewOptions +): Promise { + const streamType = stream.mediaStreamType; + + if (callId && isCallFeatureStream(stream)) { + return createCallFeatureViewVideo(context, internalContext, callId, stream, options); + } else { + _logStreamEvent(EventNames.CREATE_STREAM_INVALID_PARAMS, { streamType }); + return Promise.resolve(undefined); + } +} + +/* @conditional-compile-remove(together-mode) */ +// This function is used to create a view for a stream that is part of a call feature. +async function createCallFeatureViewVideo( + context: CallContext, + internalContext: InternalCallContext, + callId: string, + stream: TogetherModeStreamViewState, + options?: CreateViewOptions +): Promise { + const streamEventType = 'createViewCallFeature'; + + const streamType = stream?.mediaStreamType; + const callFeatureStreamId = stream && stream.id; + const streamLogInfo = { + callId, + undefined, + streamId: callFeatureStreamId, + streamType, + streamEventType + }; + + // make different logging announcement based on whether or not we are starting a local or remote + _logStreamEvent(EventNames.CREATING_VIEW, streamLogInfo); + + const featureName = getStreamFeatureName(stream); + // if we have a participant Id and a stream get the remote info, else get the local render info from state. + const renderInfo = internalContext.getCallFeatureRenderInfo(callId, featureName, stream.mediaStreamType); + if (!renderInfo) { + _logStreamEvent(EventNames.STREAM_NOT_FOUND, streamLogInfo); + return; + } + if (renderInfo.status === 'Rendered') { + _logStreamEvent(EventNames.STREAM_ALREADY_RENDERED, streamLogInfo); + return; + } + if (renderInfo.status === 'Rendering') { + // Do not log to console here as this is a very common situation due to UI rerenders while + // the video rendering is in progress. + _logStreamEvent(EventNames.STREAM_RENDERING, streamLogInfo); + return; + } + + // "Stopping" only happens if the stream was in "rendering" but `disposeView` was called. + // Now that `createView` has been re-called, we can flip the state back to "rendering". + if (renderInfo.status === 'Stopping') { + _logStreamEvent(EventNames.STREAM_STOPPING, streamLogInfo); + internalContext.setCallFeatureRenderInfo( + callId, + featureName, + stream.mediaStreamType, + renderInfo.stream, + 'Rendering', + renderInfo.renderer + ); + return; + } + + const renderer = new VideoStreamRenderer(renderInfo.stream); + internalContext.setCallFeatureRenderInfo( + callId, + featureName, + stream.mediaStreamType, + renderInfo.stream, + 'Rendering', + undefined + ); + + let view; + try { + view = await renderer.createView(options); + } catch (e) { + _logStreamEvent(EventNames.CREATE_STREAM_FAIL, streamLogInfo, e); + internalContext.setCallFeatureRenderInfo( + callId, + featureName, + stream.mediaStreamType, + renderInfo.stream, + 'NotRendered', + undefined + ); + throw e; + } + + // Since render could take some time, we need to check if the stream is still valid and if we received a signal to + // stop rendering. + const refreshedRenderInfo = internalContext.getCallFeatureRenderInfo(callId, featureName, stream.mediaStreamType); + + if (!refreshedRenderInfo) { + // RenderInfo was removed. This should not happen unless stream was removed from the call so dispose the renderer + // and clean up the state. + _logStreamEvent(EventNames.RENDER_INFO_NOT_FOUND, streamLogInfo); + renderer.dispose(); + context.setTogetherModeVideoStreamRendererView(callId, stream.mediaStreamType, undefined); + return; + } + + if (refreshedRenderInfo.status === 'Stopping') { + // Stop render was called on this stream after we had started rendering. We will dispose this view and do not + // put the view into the state. + _logStreamEvent(EventNames.CREATED_STREAM_STOPPING, streamLogInfo); + renderer.dispose(); + internalContext.setCallFeatureRenderInfo( + callId, + featureName, + stream.mediaStreamType, + refreshedRenderInfo.stream, + 'NotRendered', + undefined + ); + context.setTogetherModeVideoStreamRendererView(callId, stream.mediaStreamType, undefined); + return; + } + + // Else the stream still exists and status is not telling us to stop rendering. Complete the render process by + // updating the state. + internalContext.setCallFeatureRenderInfo( + callId, + featureName, + stream.mediaStreamType, + refreshedRenderInfo.stream, + 'Rendered', + renderer + ); + context.setTogetherModeVideoStreamRendererView( + callId, + stream.mediaStreamType, + convertFromSDKToDeclarativeVideoStreamRendererView(view) + ); + _logStreamEvent(EventNames.VIEW_RENDER_SUCCEED, streamLogInfo); + + return { + renderer, + view + }; +} + +/* @conditional-compile-remove(together-mode) */ +/** + * @private + */ +export function disposeCallFeatureView( + context: CallContext, + internalContext: InternalCallContext, + callId: string | undefined, + stream: /* @conditional-compile-remove(together-mode) */ TogetherModeStreamViewState +): void { + const streamType = stream.mediaStreamType; + if (callId && isCallFeatureStream(stream)) { + return disposeCallFeatureViewVideo(context, internalContext, callId, stream); + } else { + _logStreamEvent(EventNames.DISPOSE_STREAM_INVALID_PARAMS, { streamType }); + return; + } +} + +/* @conditional-compile-remove(together-mode) */ +/** + * @private + */ +function disposeCallFeatureViewVideo( + context: CallContext, + internalContext: InternalCallContext, + callId: string, + stream: /* @conditional-compile-remove(together-mode) */ TogetherModeStreamViewState +): void { + const streamEventType = 'disposeViewCallFeature'; + + const streamType = stream.mediaStreamType; + const callFeatureStreamId = stream && stream.id; + + const streamLogInfo = { callId, undefined, streamId: callFeatureStreamId, streamType }; + + _logStreamEvent(EventNames.START_DISPOSE_STREAM, streamLogInfo); + + const featureName = getStreamFeatureName(stream); + + if (streamEventType === 'disposeViewCallFeature') { + context.setTogetherModeVideoStreamRendererView(callId, streamType, undefined); + } + + const renderInfo = internalContext.getCallFeatureRenderInfo(callId, featureName, stream.mediaStreamType); + if (!renderInfo) { + _logStreamEvent(EventNames.DISPOSE_INFO_NOT_FOUND, streamLogInfo); + return; + } + + // Nothing to dispose of or clean up -- we can safely exit early here. + if (renderInfo.status === 'NotRendered') { + _logStreamEvent(EventNames.STREAM_ALREADY_DISPOSED, streamLogInfo); + return; + } + + // Status is already marked as "stopping" so we can exit early here. This is because stopping only occurs + // when the stream is being created in createView but hasn't been completed being created yet. The createView + // method will see the "stopping" status and perform the cleanup + if (renderInfo.status === 'Stopping') { + _logStreamEvent(EventNames.STREAM_STOPPING, streamLogInfo); + return; + } + + // If the stream is in the middle of being rendered (i.e. has state "Rendering"), we need the status as + // "stopping" without performing any cleanup. This will tell the `createView` method that it should stop + // rendering and clean up the state once the view has finished being created. + if (renderInfo.status === 'Rendering') { + _logStreamEvent(EventNames.STREAM_STOPPING, streamLogInfo); + internalContext.setCallFeatureRenderInfo( + callId, + featureName, + streamType, + renderInfo.stream, + 'Stopping', + renderInfo.renderer + ); + return; + } + + if (renderInfo.renderer) { + _logStreamEvent(EventNames.DISPOSING_RENDERER, streamLogInfo); + renderInfo.renderer.dispose(); + // Else the state must be in the "Rendered" state, so we can dispose the renderer and clean up the state. + internalContext.setCallFeatureRenderInfo( + callId, + featureName, + streamType, + renderInfo.stream, + 'NotRendered', + undefined + ); + context.setTogetherModeVideoStreamRendererView(callId, streamType, undefined); + } else { + _logStreamEvent(EventNames.RENDERER_NOT_FOUND, streamLogInfo); + } +} + +/* @conditional-compile-remove(together-mode) */ +/** + * @private + */ +const getStreamFeatureName = (stream: TogetherModeStreamViewState): string => { + if (stream.feature) { + return stream.feature; + } + throw new Error('Feature name not found'); +}; + +/* @conditional-compile-remove(together-mode) */ +/** + * @private + */ +function isCallFeatureStream(stream: CallFeatureStreamState): boolean { + return 'feature' in stream || false; +} diff --git a/packages/calling-stateful-client/src/CallSubscriber.ts b/packages/calling-stateful-client/src/CallSubscriber.ts index c1d6ba7616b..983b439161e 100644 --- a/packages/calling-stateful-client/src/CallSubscriber.ts +++ b/packages/calling-stateful-client/src/CallSubscriber.ts @@ -127,6 +127,7 @@ export class CallSubscriber { this._togetherModeSubscriber = new TogetherModeSubscriber( this._callIdRef, this._context, + this._internalContext, this._call.feature(Features.TogetherMode) ); diff --git a/packages/calling-stateful-client/src/Converter.ts b/packages/calling-stateful-client/src/Converter.ts index d65140acd75..9c8eee30563 100644 --- a/packages/calling-stateful-client/src/Converter.ts +++ b/packages/calling-stateful-client/src/Converter.ts @@ -9,6 +9,9 @@ import { IncomingCall, IncomingCallCommon } from '@azure/communication-calling'; + +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeVideoStream as SdkTogetherModeVideoStream } from '@azure/communication-calling'; import { TeamsIncomingCall } from '@azure/communication-calling'; import { TeamsCaptionsInfo } from '@azure/communication-calling'; import { CaptionsInfo as AcsCaptionsInfo } from '@azure/communication-calling'; @@ -26,6 +29,8 @@ import { VideoStreamRendererViewState as DeclarativeVideoStreamRendererView, CallInfoState } from './CallClientState'; +/* @conditional-compile-remove(calling-beta-sdk) */ +import { TogetherModeStreamViewState as DeclarativeTogetherVideoStream } from './CallClientState'; import { CaptionsInfo } from './CallClientState'; import { TeamsIncomingCallState as DeclarativeTeamsIncomingCall } from './CallClientState'; import { _isTeamsIncomingCall } from './TypeGuards'; @@ -61,7 +66,7 @@ export function convertSdkLocalStreamToDeclarativeLocalStream( * @private */ export function convertSdkRemoteStreamToDeclarativeRemoteStream( - stream: SdkRemoteVideoStream + stream: SdkRemoteVideoStream | /* @conditional-compile-remove(together-mode) */ SdkTogetherModeVideoStream ): DeclarativeRemoteVideoStream { return { id: stream.id, @@ -73,6 +78,24 @@ export function convertSdkRemoteStreamToDeclarativeRemoteStream( }; } +/* @conditional-compile-remove(together-mode) */ +/** + * @private + */ +export function convertSdkTogetherStreamToDeclarativeRemoteStream( + stream: SdkTogetherModeVideoStream +): DeclarativeTogetherVideoStream { + return { + feature: 'togetherMode', + id: stream.id, + mediaStreamType: stream.mediaStreamType, + isAvailable: stream.isAvailable, + isReceiving: stream.isReceiving, + view: undefined, + streamSize: stream.size + }; +} + /** * @private */ @@ -151,7 +174,7 @@ export function convertSdkCallToDeclarativeCall(call: CallCommon): CallState { pptLive: { isActive: false }, raiseHand: { raisedHands: [] }, /* @conditional-compile-remove(together-mode) */ - togetherMode: { stream: [] }, + togetherMode: { isActive: false, streams: {}, seatingPositions: [] }, localParticipantReaction: undefined, transcription: { isTranscriptionActive: false }, screenShareRemoteParticipant: undefined, diff --git a/packages/calling-stateful-client/src/InternalCallContext.ts b/packages/calling-stateful-client/src/InternalCallContext.ts index 500948dbf5b..a6e478c09cf 100644 --- a/packages/calling-stateful-client/src/InternalCallContext.ts +++ b/packages/calling-stateful-client/src/InternalCallContext.ts @@ -45,6 +45,12 @@ export type LocalRenderInfo = RenderInfo; */ export type RemoteRenderInfo = RenderInfo; +/* @conditional-compile-remove(together-mode) */ +/** + * Internally used to keep track of the status, renderer, and awaiting promise, associated with a CallFeatureVideoStream. + */ +export type CallFeatureRenderInfo = RenderInfo; + /** * Contains internal data used between different Declarative components to share data. */ @@ -55,6 +61,10 @@ export class InternalCallContext { // >. private _localRenderInfos = new Map>(); + /* @conditional-compile-remove(together-mode) */ + // >>. + private _callFeatureRenderInfos = new Map>>(); + // Used for keeping track of rendered LocalVideoStreams that are not part of a Call. private _unparentedRenderInfos = new Map(); private _callIdHistory = new CallIdHistory(); @@ -77,6 +87,13 @@ export class InternalCallContext { this._localRenderInfos.delete(oldCallId); this._localRenderInfos.set(newCallId, localRenderInfos); } + /* @conditional-compile-remove(together-mode) */ + const callFeatureRenderInfos = this._callFeatureRenderInfos.get(oldCallId); + /* @conditional-compile-remove(together-mode) */ + if (callFeatureRenderInfos) { + this._callFeatureRenderInfos.delete(oldCallId); + this._callFeatureRenderInfos.set(newCallId, callFeatureRenderInfos); + } } public getCallIds(): IterableIterator { @@ -221,5 +238,63 @@ export class InternalCallContext { public clearCallRelatedState(): void { this._remoteRenderInfos.clear(); this._localRenderInfos.clear(); + /* @conditional-compile-remove(together-mode) */ + this._callFeatureRenderInfos.clear(); + } + + /* @conditional-compile-remove(together-mode) */ + public getCallFeatureRenderInfosForCall( + callId: string + ): Map> | undefined { + return this._callFeatureRenderInfos.get(this._callIdHistory.latestCallId(callId)); + } + + /* @conditional-compile-remove(together-mode) */ + public getCallFeatureRenderInfo( + callId: string, + featureNameKey: string, + streamKey: MediaStreamType + ): CallFeatureRenderInfo | undefined { + const callFeatureRenderInfosForCall = this._callFeatureRenderInfos + .get(this._callIdHistory.latestCallId(callId)) + ?.get(featureNameKey) + ?.get(streamKey); + if (!callFeatureRenderInfosForCall) { + return undefined; + } + return callFeatureRenderInfosForCall; + } + + /* @conditional-compile-remove(together-mode) */ + public setCallFeatureRenderInfo( + callId: string, + featureNameKey: string, + streamKey: MediaStreamType, + stream: RemoteVideoStream, + status: RenderStatus, + renderer: VideoStreamRenderer | undefined + ): void { + let callRenderInfos = this._callFeatureRenderInfos.get(this._callIdHistory.latestCallId(callId)); + if (!callRenderInfos) { + callRenderInfos = new Map>(); + // If the callId is not found, create a new map for the callId. + this._callFeatureRenderInfos.set(this._callIdHistory.latestCallId(callId), callRenderInfos); + } + let featureRenderInfos = callRenderInfos.get(featureNameKey); + if (!featureRenderInfos) { + featureRenderInfos = new Map(); + callRenderInfos.set(featureNameKey, featureRenderInfos); + } + featureRenderInfos.set(streamKey, { stream, status, renderer }); + } + + /* @conditional-compile-remove(together-mode) */ + public deleteCallFeatureRenderInfo(callId: string, featureName: string, streamKey: MediaStreamType): void { + const callFeatureRenderInfoForCall = this._callFeatureRenderInfos.get(this._callIdHistory.latestCallId(callId)); + if (!callFeatureRenderInfoForCall || !callFeatureRenderInfoForCall.get(featureName)) { + return; + } + + callFeatureRenderInfoForCall.get(featureName)?.delete(streamKey); } } diff --git a/packages/calling-stateful-client/src/StatefulCallClient.ts b/packages/calling-stateful-client/src/StatefulCallClient.ts index a2f429e6a47..e901e356655 100644 --- a/packages/calling-stateful-client/src/StatefulCallClient.ts +++ b/packages/calling-stateful-client/src/StatefulCallClient.ts @@ -11,7 +11,7 @@ import { } from '@azure/communication-calling'; import { CallClientState, LocalVideoStreamState, RemoteVideoStreamState } from './CallClientState'; /* @conditional-compile-remove(together-mode) */ -import { TogetherModeStreamState } from './CallClientState'; +import { CallFeatureStreamState, TogetherModeStreamViewState } from './CallClientState'; import { CallContext } from './CallContext'; import { callAgentDeclaratify, DeclarativeCallAgent } from './CallAgentDeclarative'; import { InternalCallContext } from './InternalCallContext'; @@ -26,6 +26,8 @@ import { callingStatefulLogger } from './Logger'; import { DeclarativeTeamsCallAgent, teamsCallAgentDeclaratify } from './TeamsCallAgentDeclarative'; import { MicrosoftTeamsUserIdentifier } from '@azure/communication-common'; import { videoStreamRendererViewDeclaratify } from './VideoStreamRendererViewDeclarative'; +/* @conditional-compile-remove(together-mode) */ +import { createCallFeatureView, disposeCallFeatureView } from './CallFeatureStreamUtils'; /** * Defines the methods that allow CallClient {@link @azure/communication-calling#CallClient} to be used statefully. @@ -115,10 +117,7 @@ export interface StatefulCallClient extends CallClient { createView( callId: string | undefined, participantId: CommunicationIdentifier | undefined, - stream: - | LocalVideoStreamState - | RemoteVideoStreamState - | /* @conditional-compile-remove(together-mode) */ TogetherModeStreamState, + stream: LocalVideoStreamState | RemoteVideoStreamState, options?: CreateViewOptions ): Promise; /** @@ -151,6 +150,44 @@ export interface StatefulCallClient extends CallClient { stream: LocalVideoStreamState | RemoteVideoStreamState ): void; + /* @conditional-compile-remove(together-mode) */ + /** + * Renders a {@link CallFeatureStreamState} + * {@link VideoStreamRendererViewState} under the relevant {@link CallFeatureStreamState} + * {@link @azure/communication-calling#VideoStreamRenderer.createView}. + * + * Scenario 1: Render CallFeatureStreamState + * - CallId is required and stream of type CallFeatureStreamState is required + * - Resulting {@link VideoStreamRendererViewState} is stored in the given callId and participantId in + * {@link CallClientState} + * + * @param callId - CallId for the given stream. Can be undefined if the stream is not part of any call. + * @param stream - The LocalVideoStreamState or RemoteVideoStreamState to start rendering. + * @param options - Options that are passed to the {@link @azure/communication-calling#VideoStreamRenderer}. + * @beta + */ + createCallFeatureView( + callId: string, + stream: CallFeatureStreamState, + options?: CreateViewOptions + ): Promise; + + /* @conditional-compile-remove(together-mode) */ + /** + * Stops rendering a {@link CallFeatureStreamState} and removes the + * {@link VideoStreamRendererView} from the relevant {@link CallFeatureStreamState} in {@link CallClientState} or + * {@link @azure/communication-calling#VideoStreamRenderer.dispose}. + * + * Its important to disposeView to clean up resources properly. + * + * Scenario 1: Dispose CallFeatureStreamState + * - CallId is required and stream of type CallFeatureStreamState is required + * + * @param callId - CallId for the given stream. Can be undefined if the stream is not part of any call. + * @param stream - The LocalVideoStreamState or RemoteVideoStreamState to dispose. + * @beta + */ + disposeCallFeatureView(callId: string, stream: CallFeatureStreamState): void; /** * The CallAgent is used to handle calls. * To create the CallAgent, pass a CommunicationTokenCredential object provided from SDK. @@ -399,6 +436,18 @@ export const createStatefulCallClientWithDeps = ( return result; } }); + /* @conditional-compile-remove(together-mode) */ + Object.defineProperty(callClient, 'createCallFeatureView', { + configurable: false, + value: async ( + callId: string | undefined, + stream: TogetherModeStreamViewState, + options?: CreateViewOptions + ): Promise => { + const result = await createCallFeatureView(context, internalContext, callId, stream, options); + return result; + } + }); Object.defineProperty(callClient, 'disposeView', { configurable: false, value: ( @@ -410,6 +459,13 @@ export const createStatefulCallClientWithDeps = ( disposeView(context, internalContext, callId, participantIdKind, stream); } }); + /* @conditional-compile-remove(together-mode) */ + Object.defineProperty(callClient, 'disposeCallFeatureView', { + configurable: false, + value: (callId: string | undefined, stream: TogetherModeStreamViewState): void => { + disposeCallFeatureView(context, internalContext, callId, stream); + } + }); const newStatefulCallClient = new Proxy( callClient, diff --git a/packages/calling-stateful-client/src/StreamUtils.test.ts b/packages/calling-stateful-client/src/StreamUtils.test.ts index 02dc616a08a..9840e19adb1 100644 --- a/packages/calling-stateful-client/src/StreamUtils.test.ts +++ b/packages/calling-stateful-client/src/StreamUtils.test.ts @@ -88,7 +88,7 @@ function createMockCall(mockCallId: string): CallState { localRecording: { isLocalRecordingActive: false }, raiseHand: { raisedHands: [] }, /* @conditional-compile-remove(together-mode) */ - togetherMode: { stream: [] }, + togetherMode: { isActive: false, streams: {}, seatingPositions: [] }, localParticipantReaction: undefined, transcription: { isTranscriptionActive: false }, screenShareRemoteParticipant: undefined, diff --git a/packages/calling-stateful-client/src/StreamUtils.ts b/packages/calling-stateful-client/src/StreamUtils.ts index 30d8339bc88..4ca1296e0b6 100644 --- a/packages/calling-stateful-client/src/StreamUtils.ts +++ b/packages/calling-stateful-client/src/StreamUtils.ts @@ -10,8 +10,6 @@ import { } from '@azure/communication-calling'; import { CommunicationIdentifierKind } from '@azure/communication-common'; import { LocalVideoStreamState, RemoteVideoStreamState } from './CallClientState'; -/* @conditional-compile-remove(together-mode) */ -import { TogetherModeStreamState } from './CallClientState'; import { CallContext } from './CallContext'; import { convertSdkLocalStreamToDeclarativeLocalStream, @@ -37,10 +35,7 @@ async function createViewVideo( context: CallContext, internalContext: InternalCallContext, callId: string, - stream?: - | RemoteVideoStreamState - | LocalVideoStreamState - | /* @conditional-compile-remove(together-mode) */ TogetherModeStreamState, + stream?: RemoteVideoStreamState | LocalVideoStreamState, participantId?: CommunicationIdentifierKind | string, options?: CreateViewOptions ): Promise { @@ -493,10 +488,7 @@ export function createView( internalContext: InternalCallContext, callId: string | undefined, participantId: CommunicationIdentifierKind | string | undefined, - stream: - | LocalVideoStreamState - | RemoteVideoStreamState - | /* @conditional-compile-remove(together-mode) */ TogetherModeStreamState, + stream: LocalVideoStreamState | RemoteVideoStreamState, options?: CreateViewOptions ): Promise { const streamType = stream.mediaStreamType; @@ -586,6 +578,22 @@ export function disposeAllViewsFromCall( } } } + /* @conditional-compile-remove(together-mode) */ + const callFeatureStreams = internalContext.getCallFeatureRenderInfosForCall(callId); + /* @conditional-compile-remove(together-mode) */ + if (callFeatureStreams) { + for (const [, featureStreams] of callFeatureStreams.entries()) { + for (const [, streamAndRenderer] of featureStreams.entries()) { + disposeView( + context, + internalContext, + callId, + undefined, + convertSdkRemoteStreamToDeclarativeRemoteStream(streamAndRenderer.stream as RemoteVideoStream) + ); + } + } + } } /** diff --git a/packages/calling-stateful-client/src/TogetherModeSubscriber.ts b/packages/calling-stateful-client/src/TogetherModeSubscriber.ts index 64f791becc0..999ac82808a 100644 --- a/packages/calling-stateful-client/src/TogetherModeSubscriber.ts +++ b/packages/calling-stateful-client/src/TogetherModeSubscriber.ts @@ -2,15 +2,27 @@ // Licensed under the MIT License. /* @conditional-compile-remove(together-mode) */ -import { TogetherModeCallFeature, TogetherModeVideoStream } from '@azure/communication-calling'; +import { + RemoteVideoStream, + TogetherModeCallFeature, + TogetherModeSeatingMap, + TogetherModeVideoStream +} from '@azure/communication-calling'; /* @conditional-compile-remove(together-mode) */ import { CallContext } from './CallContext'; /* @conditional-compile-remove(together-mode) */ import { CallIdRef } from './CallIdRef'; -/** - * @private - */ - +/* @conditional-compile-remove(together-mode) */ +import { InternalCallContext } from './InternalCallContext'; +/* @conditional-compile-remove(together-mode) */ +import { disposeCallFeatureView } from './CallFeatureStreamUtils'; +/* @conditional-compile-remove(together-mode) */ +import { + convertSdkRemoteStreamToDeclarativeRemoteStream, + convertSdkTogetherStreamToDeclarativeRemoteStream +} from './Converter'; +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeVideoStreamSubscriber } from './TogetherModeVideoStreamSubscriber'; /* @conditional-compile-remove(together-mode) */ /** * TogetherModeSubscriber is responsible for subscribing to together mode events and updating the call context accordingly. @@ -18,33 +30,94 @@ import { CallIdRef } from './CallIdRef'; export class TogetherModeSubscriber { private _callIdRef: CallIdRef; private _context: CallContext; + private _internalContext: InternalCallContext; private _togetherMode: TogetherModeCallFeature; + private _featureName = 'togetherMode'; + private _togetherModeVideoStreamSubscribers: Map; - constructor(callIdRef: CallIdRef, context: CallContext, togetherMode: TogetherModeCallFeature) { + constructor( + callIdRef: CallIdRef, + context: CallContext, + internalContext: InternalCallContext, + togetherMode: TogetherModeCallFeature + ) { this._callIdRef = callIdRef; this._context = context; + this._internalContext = internalContext; this._togetherMode = togetherMode; - + this._togetherModeVideoStreamSubscribers = new Map(); this.subscribe(); } private subscribe = (): void => { this._togetherMode.on('togetherModeStreamsUpdated', this.onTogetherModeStreamUpdated); + this._togetherMode.on('togetherModeSceneUpdated', this.onSceneUpdated); + this._togetherMode.on('togetherModeSeatingUpdated', this.onSeatUpdated); }; public unsubscribe = (): void => { this._togetherMode.off('togetherModeStreamsUpdated', this.onTogetherModeStreamUpdated); + this._togetherMode.off('togetherModeSceneUpdated', this.onSceneUpdated); + this._togetherMode.off('togetherModeSeatingUpdated', this.onSeatUpdated); + }; + + private onSceneUpdated = (args: TogetherModeSeatingMap): void => { + this._context.setTogetherModeSeatingCoordinates(this._callIdRef.callId, args); + }; + + private onSeatUpdated = (args: TogetherModeSeatingMap): void => { + this._context.setTogetherModeSeatingCoordinates(this._callIdRef.callId, args); + }; + + private addRemoteVideoStreamSubscriber = (togetherModeVideoStream: TogetherModeVideoStream): void => { + this._togetherModeVideoStreamSubscribers.get(togetherModeVideoStream.id)?.unsubscribe(); + this._togetherModeVideoStreamSubscribers.set( + togetherModeVideoStream.id, + new TogetherModeVideoStreamSubscriber(this._callIdRef, togetherModeVideoStream, this._context) + ); + }; + + private updateTogetherModeStreams = ( + addedStreams: TogetherModeVideoStream[], + removedStreams: TogetherModeVideoStream[] + ): void => { + for (const stream of removedStreams) { + this._togetherModeVideoStreamSubscribers.get(stream.id)?.unsubscribe(); + disposeCallFeatureView( + this._context, + this._internalContext, + this._callIdRef.callId, + convertSdkRemoteStreamToDeclarativeRemoteStream(stream) + ); + this._internalContext.deleteCallFeatureRenderInfo( + this._callIdRef.callId, + this._featureName, + stream.mediaStreamType + ); + } + + for (const stream of addedStreams) { + this._internalContext.setCallFeatureRenderInfo( + this._callIdRef.callId, + this._featureName, + stream.mediaStreamType, + stream as RemoteVideoStream, + 'NotRendered', + undefined + ); + this.addRemoteVideoStreamSubscriber(stream); + } + this._context.setTogetherModeVideoStreams( + this._callIdRef.callId, + addedStreams.map(convertSdkTogetherStreamToDeclarativeRemoteStream), + removedStreams.map(convertSdkTogetherStreamToDeclarativeRemoteStream) + ); }; private onTogetherModeStreamUpdated = (args: { added: TogetherModeVideoStream[]; removed: TogetherModeVideoStream[]; }): void => { - if (args.added) { - this._context.setTogetherModeVideoStream(this._callIdRef.callId, args.added); - } - if (args.removed) { - this._context.removeTogetherModeVideoStream(this._callIdRef.callId, args.removed); - } + this.updateTogetherModeStreams(args.added, args.removed); }; } diff --git a/packages/calling-stateful-client/src/TogetherModeVideoStreamSubscriber.ts b/packages/calling-stateful-client/src/TogetherModeVideoStreamSubscriber.ts new file mode 100644 index 00000000000..5ce633273ce --- /dev/null +++ b/packages/calling-stateful-client/src/TogetherModeVideoStreamSubscriber.ts @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeVideoStream } from '@azure/communication-calling'; +/* @conditional-compile-remove(together-mode) */ +import { CallContext } from './CallContext'; +/* @conditional-compile-remove(together-mode) */ +import { CallIdRef } from './CallIdRef'; + +/* @conditional-compile-remove(together-mode) */ +/** + * @private + */ +export class TogetherModeVideoStreamSubscriber { + private _callIdRef: CallIdRef; + private _togetherModeStream: TogetherModeVideoStream; + private _context: CallContext; + + constructor(callIdRef: CallIdRef, stream: TogetherModeVideoStream, context: CallContext) { + this._callIdRef = callIdRef; + this._togetherModeStream = stream; + this._context = context; + this.subscribe(); + } + + private subscribe = (): void => { + this._togetherModeStream.on('isAvailableChanged', this.isAvailableChanged); + this._togetherModeStream.on('isReceivingChanged', this.isReceivingChanged); + this._togetherModeStream.on('sizeChanged', this.isSizeChanged); + }; + + public unsubscribe = (): void => { + this._togetherModeStream.off('isAvailableChanged', this.isAvailableChanged); + this._togetherModeStream.off('isReceivingChanged', this.isReceivingChanged); + this._togetherModeStream.off('sizeChanged', this.isSizeChanged); + }; + + private isAvailableChanged = (): void => { + this._context.setTogetherModeVideoStreamIsAvailable( + this._callIdRef.callId, + this._togetherModeStream.id, + this._togetherModeStream.isAvailable + ); + }; + + private isReceivingChanged = (): void => { + this._context.setTogetherModeVideoStreamIsReceiving( + this._callIdRef.callId, + this._togetherModeStream.id, + this._togetherModeStream.isReceiving + ); + }; + + private isSizeChanged = (): void => { + this._context.setTogetherModeVideoStreamSize( + this._callIdRef.callId, + this._togetherModeStream.id, + this._togetherModeStream.size + ); + }; +} diff --git a/packages/calling-stateful-client/src/index-public.ts b/packages/calling-stateful-client/src/index-public.ts index 72fc02446d6..817ae92fe1a 100644 --- a/packages/calling-stateful-client/src/index-public.ts +++ b/packages/calling-stateful-client/src/index-public.ts @@ -34,7 +34,14 @@ export type { RaiseHandCallFeatureState as RaiseHandCallFeature } from './CallCl /* @conditional-compile-remove(together-mode) */ export type { TogetherModeCallFeatureState as TogetherModeCallFeature } from './CallClientState'; /* @conditional-compile-remove(together-mode) */ -export type { TogetherModeStreamState } from './CallClientState'; +export type { + CallFeatureStreamState, + TogetherModeStreamViewState, + TogetherModeSeatingPositionState, + CallFeatureStreamName, + TogetherModeStreamsState +} from './CallClientState'; + export type { RaisedHandState } from './CallClientState'; export type { DeclarativeCallAgent, IncomingCallManagement } from './CallAgentDeclarative'; export type { DeclarativeTeamsCallAgent } from './TeamsCallAgentDeclarative'; diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index cbbbed7f4de..acd352fbbea 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -111,6 +111,8 @@ import { TeamsIncomingCall } from '@azure/communication-calling'; import { TeamsMeetingIdLocator } from '@azure/communication-calling'; import { TeamsMeetingLinkLocator } from '@azure/communication-calling'; import { Theme } from '@fluentui/react'; +import { TogetherModeStreamViewResult } from '@internal/react-components/dist/dist-esm/types'; +import { TogetherModeStreamViewResult as TogetherModeStreamViewResult_2 } from '@internal/react-components/dist/dist-esm/types/TogetherModeTypes'; import { TransferEventArgs } from '@azure/communication-calling'; import { TypingIndicatorReceivedEvent } from '@azure/communication-chat'; import { UnknownIdentifier } from '@azure/communication-common'; @@ -442,11 +444,15 @@ export interface CallAdapterCallOperations { addParticipant(participant: CommunicationUserIdentifier): Promise; allowUnsupportedBrowserVersion(): void; createStreamView(remoteUserId?: string, options?: VideoStreamOptions): Promise; + // @beta + createTogetherModeStreamViews(options?: VideoStreamOptions): Promise; disposeLocalVideoStreamView(): Promise; disposeRemoteVideoStreamView(remoteUserId: string): Promise; disposeScreenShareStreamView(remoteUserId: string): Promise; // @deprecated disposeStreamView(remoteUserId?: string, options?: VideoStreamOptions): Promise; + // @beta + disposeTogetherModeStreamViews(): Promise; holdCall(): Promise; leaveCall(forEveryone?: boolean): Promise; lowerHand(): Promise; @@ -1036,6 +1042,15 @@ export type CallErrors = { // @public export type CallErrorTarget = 'Call.addParticipant' | 'Call.dispose' | 'Call.feature' | 'Call.hangUp' | 'Call.hold' | 'Call.mute' | 'Call.muteIncomingAudio' | 'Call.off' | 'Call.on' | 'Call.removeParticipant' | 'Call.resume' | 'Call.sendDtmf' | 'Call.startAudio' | 'Call.startScreenSharing' | 'Call.startVideo' | 'Call.stopScreenSharing' | 'Call.stopAudio' | 'Call.stopVideo' | 'Call.unmute' | 'Call.unmuteIncomingAudio' | 'CallAgent.dispose' | 'CallAgent.feature' | 'CallAgent.join' | 'CallAgent.off' | 'CallAgent.on' | 'CallAgent.startCall' | 'CallClient.createCallAgent' | 'CallClient.createTeamsCallAgent' | 'CallClient.feature' | 'CallClient.getDeviceManager' | 'CallClient.getEnvironmentInfo' | 'DeviceManager.askDevicePermission' | 'DeviceManager.getCameras' | 'DeviceManager.getMicrophones' | 'DeviceManager.getSpeakers' | 'DeviceManager.off' | 'DeviceManager.on' | 'DeviceManager.selectMicrophone' | 'DeviceManager.selectSpeaker' | 'IncomingCall.accept' | 'IncomingCall.reject' | 'TeamsCall.addParticipant' | 'VideoEffectsFeature.startEffects' | /* @conditional-compile-remove(calling-beta-sdk) */ 'CallAgent.handlePushNotification' | /* @conditional-compile-remove(calling-beta-sdk) */ 'Call.admit' | /* @conditional-compile-remove(calling-beta-sdk) */ 'Call.rejectParticipant' | /* @conditional-compile-remove(calling-beta-sdk) */ 'Call.admitAll' | 'Call.mutedByOthers' | 'Call.muteAllRemoteParticipants' | 'Call.setConstraints'; +// @beta (undocumented) +export type CallFeatureStreamName = 'togetherMode'; + +// @beta (undocumented) +export interface CallFeatureStreamState { + // (undocumented) + feature?: CallFeatureStreamName; +} + // @public export type CallIdChangedListener = (event: { callId: string; @@ -1168,6 +1183,7 @@ export interface CallState { spotlight?: SpotlightCallFeatureState; startTime: Date; state: CallState_2; + // @beta togetherMode: TogetherModeCallFeature; totalParticipantCount?: number; transcription: TranscriptionCallFeature; @@ -1195,12 +1211,16 @@ export interface CallWithChatAdapterManagement { askDevicePermission(constrain: PermissionConstraints): Promise; createStreamView(remoteUserId?: string, options?: VideoStreamOptions): Promise; // @beta + createTogetherModeStreamViews(options?: VideoStreamOptions): Promise; + // @beta deleteImage(imageId: string): Promise; deleteMessage(messageId: string): Promise; disposeLocalVideoStreamView(): Promise; disposeRemoteVideoStreamView(remoteUserId: string): Promise; disposeScreenShareStreamView(remoteUserId: string): Promise; disposeStreamView(remoteUserId?: string, options?: VideoStreamOptions): Promise; + // @beta + disposeTogetherModeStreamViews(): Promise; // (undocumented) downloadResourceToCache(resourceDetails: ResourceDetails): Promise; fetchInitialData(): Promise; @@ -2173,6 +2193,8 @@ export interface CommonCallingHandlers { onCreateLocalStreamView: (options?: VideoStreamOptions) => Promise; // (undocumented) onCreateRemoteStreamView: (userId: string, options?: VideoStreamOptions) => Promise; + // @beta + onCreateTogetherModeStreamView: (options?: VideoStreamOptions) => Promise; // (undocumented) onDisposeLocalScreenShareStreamView: () => Promise; // (undocumented) @@ -2183,6 +2205,8 @@ export interface CommonCallingHandlers { onDisposeRemoteStreamView: (userId: string) => Promise; // (undocumented) onDisposeRemoteVideoStreamView: (userId: string) => Promise; + // @beta + onDisposeTogetherModeStreamViews: () => Promise; // (undocumented) onHangUp: (forEveryone?: boolean) => Promise; // (undocumented) @@ -4684,8 +4708,12 @@ export type StartTeamsCallIdentifier = MicrosoftTeamsUserIdentifier | PhoneNumbe // @public export interface StatefulCallClient extends CallClient { createCallAgent(...args: Parameters): Promise; + // @beta + createCallFeatureView(callId: string, stream: CallFeatureStreamState, options?: CreateViewOptions): Promise; createTeamsCallAgent(...args: Parameters): Promise; - createView(callId: string | undefined, participantId: CommunicationIdentifier | undefined, stream: LocalVideoStreamState | RemoteVideoStreamState | /* @conditional-compile-remove(together-mode) */ TogetherModeStreamState, options?: CreateViewOptions): Promise; + createView(callId: string | undefined, participantId: CommunicationIdentifier | undefined, stream: LocalVideoStreamState | RemoteVideoStreamState, options?: CreateViewOptions): Promise; + // @beta + disposeCallFeatureView(callId: string, stream: CallFeatureStreamState): void; disposeView(callId: string | undefined, participantId: CommunicationIdentifier | undefined, stream: LocalVideoStreamState | RemoteVideoStreamState): void; getState(): CallClientState; offStateChange(handler: (state: CallClientState) => void): void; @@ -4871,22 +4899,36 @@ export type TeamsOutboundCallAdapterArgs = TeamsCallAdapterArgsCommon & { // @public export const toFlatCommunicationIdentifier: (identifier: CommunicationIdentifier) => string; -// @alpha +// @beta export interface TogetherModeCallFeature { - stream: TogetherModeStreamState[]; + // (undocumented) + isActive: boolean; + seatingPositions: TogetherModeSeatingPositionState[]; + streams: TogetherModeStreamsState; } -// @alpha -export interface TogetherModeStreamState { - id: number; - // @public - isReceiving: boolean; - mediaStreamType: MediaStreamType; - streamSize?: { - width: number; - height: number; - }; - view?: VideoStreamRendererViewState; +// @beta +export interface TogetherModeSeatingPositionState { + // (undocumented) + height: number; + // (undocumented) + left: number; + // (undocumented) + participantId: string; + // (undocumented) + top: number; + // (undocumented) + width: number; +} + +// @beta +export interface TogetherModeStreamsState { + // (undocumented) + mainVideoStream?: TogetherModeStreamViewState; +} + +// @beta +export interface TogetherModeStreamViewState extends RemoteVideoStreamState, CallFeatureStreamState { } // @public diff --git a/packages/react-components/src/localization/locales/cy-GB/strings.json b/packages/react-components/src/localization/locales/cy-GB/strings.json index 62d0cc40d33..103c6d20658 100644 --- a/packages/react-components/src/localization/locales/cy-GB/strings.json +++ b/packages/react-components/src/localization/locales/cy-GB/strings.json @@ -667,4 +667,4 @@ "incomingCallNotificationAccceptWithVideoButtonLabel": "Derbyn gyda Fideo", "incomingCallNotificationDismissButtonAriaLabel": "Diystyru" } -} \ No newline at end of file +} diff --git a/packages/react-components/src/types/TogetherModeTypes.ts b/packages/react-components/src/types/TogetherModeTypes.ts new file mode 100644 index 00000000000..6f4a8c8c28f --- /dev/null +++ b/packages/react-components/src/types/TogetherModeTypes.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { CreateVideoStreamViewResult } from './VideoGalleryParticipant'; + +/** + * Interface representing the result of a Together Mode stream view. + */ +export interface TogetherModeStreamViewResult { + mainVideoView?: CreateVideoStreamViewResult; + panomaricVideoView?: CreateVideoStreamViewResult; +} diff --git a/packages/react-components/src/types/index.ts b/packages/react-components/src/types/index.ts index 07b84e17660..a1ce7ab3381 100644 --- a/packages/react-components/src/types/index.ts +++ b/packages/react-components/src/types/index.ts @@ -15,3 +15,4 @@ export * from './SurveyIssuesHeadingStrings'; export * from './CallSurveyImprovementSuggestions'; export * from './ReactionTypes'; export * from './Attachment'; +export * from './TogetherModeTypes'; diff --git a/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts b/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts index 40c6dec15de..288c26e7e79 100644 --- a/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts +++ b/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts @@ -110,9 +110,18 @@ export class _MockCallAdapter implements CallAdapter { createStreamView(): Promise { throw Error('createStreamView not implemented'); } + /* @conditional-compile-remove(together-mode) */ + createTogetherModeStreamViews(): Promise { + throw Error('createTogetherModeStreamViews not implemented'); + } + /* @conditional-compile-remove(together-mode) */ startTogetherMode(): Promise { throw Error('startTogetherMode not implemented'); } + /* @conditional-compile-remove(together-mode) */ + setTogetherModeSceneSize(width: number, height: number): void { + throw Error(`Setting Together Mode scene to width ${width} and height ${height} is not implemented`); + } disposeStreamView(): Promise { return Promise.resolve(); } @@ -125,6 +134,10 @@ export class _MockCallAdapter implements CallAdapter { disposeRemoteVideoStreamView(): Promise { return Promise.resolve(); } + /* @conditional-compile-remove(together-mode) */ + disposeTogetherModeStreamViews(): Promise { + return Promise.resolve(); + } // eslint-disable-next-line @typescript-eslint/no-unused-vars askDevicePermission(constrain: PermissionConstraints): Promise { throw Error('askDevicePermission not implemented'); @@ -257,7 +270,7 @@ const createDefaultCallAdapterState = (role?: ParticipantRole): CallAdapterState remoteParticipantsEnded: {}, raiseHand: { raisedHands: [] }, /* @conditional-compile-remove(together-mode) */ - togetherMode: { stream: [] }, + togetherMode: { isActive: false, streams: {}, seatingPositions: [] }, pptLive: { isActive: false }, localParticipantReaction: undefined, role, diff --git a/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts b/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts index d6d87ef5711..6c0029f0c77 100644 --- a/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts +++ b/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts @@ -116,6 +116,8 @@ import { import { CallSurvey, CallSurveyResponse } from '@azure/communication-calling'; import { CallingSoundSubscriber } from './CallingSoundSubscriber'; import { CallingSounds } from './CallAdapter'; +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeStreamViewResult } from '@internal/react-components/dist/dist-esm/types'; type CallTypeOf = AgentType extends CallAgent ? Call : TeamsCall; /** @@ -596,6 +598,10 @@ export class AzureCommunicationCallAdapter { + return await this.handlers.onCreateTogetherModeStreamView(options); + } + + /* @conditional-compile-remove(together-mode) */ + public async disposeTogetherModeStreamViews(): Promise { + return await this.handlers.onDisposeTogetherModeStreamViews(); + } + public async leaveCall(forEveryone?: boolean): Promise { if (this.getState().page === 'transferring') { const transferCall = this.callAgent.calls.filter( diff --git a/packages/react-composites/src/composites/CallComposite/adapter/CallAdapter.ts b/packages/react-composites/src/composites/CallComposite/adapter/CallAdapter.ts index c3e4d980a08..cafb88b002c 100644 --- a/packages/react-composites/src/composites/CallComposite/adapter/CallAdapter.ts +++ b/packages/react-composites/src/composites/CallComposite/adapter/CallAdapter.ts @@ -39,7 +39,8 @@ import { } from '@internal/calling-component-bindings'; import { CallSurvey, CallSurveyResponse } from '@azure/communication-calling'; import { ReactionResources } from '@internal/react-components'; - +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeStreamViewResult } from '@internal/react-components/dist/dist-esm/types'; /** * Major UI screens shown in the {@link CallComposite}. * @@ -609,6 +610,34 @@ export interface CallAdapterCallOperations { * @public */ disposeStreamView(remoteUserId?: string, options?: VideoStreamOptions): Promise; + /* @conditional-compile-remove(together-mode) */ + /** + * Create the html view for a stream. + * + * @remarks + * This method is implemented for composite + * + * @param featureName - Name of feature to render + * @param options - Options to control how video streams are rendered {@link @azure/communication-calling#VideoStreamOptions } + * + * @beta + */ + createTogetherModeStreamViews(options?: VideoStreamOptions): Promise; + + /* @conditional-compile-remove(together-mode) */ + /** + * Dispose the html view for a stream. + * + * @remarks + * This method is implemented for composite + * + * + * @param featureName - Name of the feature to dispose + * @param options - Options to control how video streams are rendered {@link @azure/communication-calling#VideoStreamOptions } + * + * @beta + */ + disposeTogetherModeStreamViews(): Promise; /** * Dispose the html view for a screen share stream * diff --git a/packages/react-composites/src/composites/CallComposite/hooks/useHandlers.ts b/packages/react-composites/src/composites/CallComposite/hooks/useHandlers.ts index e5baab982ca..eabeff5b037 100644 --- a/packages/react-composites/src/composites/CallComposite/hooks/useHandlers.ts +++ b/packages/react-composites/src/composites/CallComposite/hooks/useHandlers.ts @@ -228,6 +228,14 @@ const createCompositeHandlers = memoizeOne( }, onMuteAllRemoteParticipants: async (): Promise => { await adapter.muteAllRemoteParticipants(); + }, + /* @conditional-compile-remove(together-mode) */ + onCreateTogetherModeStreamView: async (options) => { + return await adapter.createTogetherModeStreamViews(options); + }, + /* @conditional-compile-remove(together-mode) */ + onDisposeTogetherModeStreamViews: async () => { + return await adapter.disposeTogetherModeStreamViews(); } }; } diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts index d992339c041..dacc9b662b6 100644 --- a/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts +++ b/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts @@ -107,6 +107,8 @@ import { VideoBackgroundImage, VideoBackgroundEffect } from '../../CallComposite import { CallSurvey, CallSurveyResponse } from '@azure/communication-calling'; /* @conditional-compile-remove(breakout-rooms) */ import { busyWait } from '../../common/utils'; +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeStreamViewResult } from '@internal/react-components/dist/dist-esm/types'; type CallWithChatAdapterStateChangedHandler = (newState: CallWithChatAdapterState) => void; @@ -518,6 +520,16 @@ export class AzureCommunicationCallWithChatAdapter implements CallWithChatAdapte public async disposeLocalVideoStreamView(): Promise { await this.callAdapter.disposeLocalVideoStreamView(); } + /* @conditional-compile-remove(together-mode) */ + public async createTogetherModeStreamViews( + options?: VideoStreamOptions + ): Promise { + return await this.callAdapter.createTogetherModeStreamViews(options); + } + /* @conditional-compile-remove(together-mode) */ + public async disposeTogetherModeStreamViews(): Promise { + await this.callAdapter.disposeTogetherModeStreamViews(); + } /** Fetch initial Call and Chat data such as chat messages. */ public async fetchInitialData(): Promise { return await this.executeWithResolvedChatAdapter((adapter) => { diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts index e11aa98927a..55052009127 100644 --- a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts +++ b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts @@ -69,6 +69,8 @@ import { SpotlightChangedListener } from '../../CallComposite/adapter/CallAdapte import { VideoBackgroundImage, VideoBackgroundEffect } from '../../CallComposite'; import { CallSurvey, CallSurveyResponse } from '@azure/communication-calling'; +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeStreamViewResult } from '@internal/react-components/dist/dist-esm/types'; /** * Functionality for managing the current call with chat. @@ -227,6 +229,29 @@ export interface CallWithChatAdapterManagement { * @public */ disposeStreamView(remoteUserId?: string, options?: VideoStreamOptions): Promise; + /* @conditional-compile-remove(together-mode) */ + /** + * Create the html view for a stream. + * + * @remarks + * This method is implemented for composite + * + * @param options - Options to control how video streams are rendered {@link @azure/communication-calling#VideoStreamOptions } + * + * @beta + */ + createTogetherModeStreamViews(options?: VideoStreamOptions): Promise; + + /* @conditional-compile-remove(together-mode) */ + /** + * Dispose the html view for a stream. + * + * @remarks + * This method is implemented for composite + * + * @beta + */ + disposeTogetherModeStreamViews(): Promise; /** * Dispose the html view for a screen share stream * diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts index e25f049c4e1..6c13b84ee1e 100644 --- a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts +++ b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts @@ -28,6 +28,8 @@ import { StopCaptionsAdapterOptions } from '../../CallComposite/adapter/CallAdapter'; import { CallSurvey, CallSurveyResponse } from '@azure/communication-calling'; +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeStreamViewResult } from '@internal/react-components/dist/dist-esm/types'; /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ @@ -135,6 +137,11 @@ export class CallWithChatBackedCallAdapter implements CallAdapter { options?: VideoStreamOptions ): Promise => await this.callWithChatAdapter.createStreamView(remoteUserId, options); + /* @conditional-compile-remove(together-mode) */ + public createTogetherModeStreamViews = async ( + options?: VideoStreamOptions + ): Promise => + await this.callWithChatAdapter.createTogetherModeStreamViews(options); public disposeStreamView = async (remoteUserId?: string, options?: VideoStreamOptions): Promise => await this.callWithChatAdapter.disposeStreamView(remoteUserId, options); public disposeScreenShareStreamView(remoteUserId: string): Promise { @@ -146,6 +153,9 @@ export class CallWithChatBackedCallAdapter implements CallAdapter { public disposeLocalVideoStreamView(): Promise { return this.callWithChatAdapter.disposeLocalVideoStreamView(); } + /* @conditional-compile-remove(together-mode) */ + public disposeTogetherModeStreamViews = async (): Promise => + await this.callWithChatAdapter.disposeTogetherModeStreamViews(); public holdCall = async (): Promise => { await this.callWithChatAdapter.holdCall(); }; diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/TestUtils.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/TestUtils.ts index 5d9a8d99e59..79cb9a2c15b 100644 --- a/packages/react-composites/src/composites/CallWithChatComposite/adapter/TestUtils.ts +++ b/packages/react-composites/src/composites/CallWithChatComposite/adapter/TestUtils.ts @@ -240,7 +240,7 @@ function createMockCall(mockCallId: string): CallState { dominantSpeakers: undefined, raiseHand: { raisedHands: [] }, /* @conditional-compile-remove(together-mode) */ - togetherMode: { stream: [] }, + togetherMode: { isActive: false, streams: {}, seatingPositions: [] }, pptLive: { isActive: false }, localParticipantReaction: undefined, captionsFeature: { diff --git a/packages/react-composites/src/composites/localization/locales/ar-SA/strings.json b/packages/react-composites/src/composites/localization/locales/ar-SA/strings.json index 5f7bf8364ba..47e89a2cbd3 100644 --- a/packages/react-composites/src/composites/localization/locales/ar-SA/strings.json +++ b/packages/react-composites/src/composites/localization/locales/ar-SA/strings.json @@ -409,4 +409,4 @@ "breakoutRoomChatPaneTitle": "دردشة الغرفة الفرعية", "chatContentSpinnerLabel": "يتم الآن التحميل..." } -} \ No newline at end of file +} diff --git a/packages/react-composites/src/composites/localization/locales/cs-CZ/strings.json b/packages/react-composites/src/composites/localization/locales/cs-CZ/strings.json index c347c60e8c8..35f9533df54 100644 --- a/packages/react-composites/src/composites/localization/locales/cs-CZ/strings.json +++ b/packages/react-composites/src/composites/localization/locales/cs-CZ/strings.json @@ -409,4 +409,4 @@ "breakoutRoomChatPaneTitle": "Chat ve skupinové místnosti", "chatContentSpinnerLabel": "Načítání..." } -} \ No newline at end of file +} diff --git a/packages/react-composites/src/composites/localization/locales/cy-GB/strings.json b/packages/react-composites/src/composites/localization/locales/cy-GB/strings.json index b3d9b859776..8e140d83eed 100644 --- a/packages/react-composites/src/composites/localization/locales/cy-GB/strings.json +++ b/packages/react-composites/src/composites/localization/locales/cy-GB/strings.json @@ -409,4 +409,4 @@ "breakoutRoomChatPaneTitle": "Sgwrs y Cyfarfod Ymylol", "chatContentSpinnerLabel": "Wrthi'n llwytho..." } -} \ No newline at end of file +} diff --git a/packages/react-composites/src/composites/localization/locales/de-DE/strings.json b/packages/react-composites/src/composites/localization/locales/de-DE/strings.json index 78d088e45b9..c30a426951f 100644 --- a/packages/react-composites/src/composites/localization/locales/de-DE/strings.json +++ b/packages/react-composites/src/composites/localization/locales/de-DE/strings.json @@ -409,4 +409,4 @@ "breakoutRoomChatPaneTitle": "Chat im Gruppenraum", "chatContentSpinnerLabel": "Wird geladen..." } -} \ No newline at end of file +} diff --git a/packages/react-composites/src/composites/localization/locales/en-GB/strings.json b/packages/react-composites/src/composites/localization/locales/en-GB/strings.json index 118a0804136..2ae2f22a2f0 100644 --- a/packages/react-composites/src/composites/localization/locales/en-GB/strings.json +++ b/packages/react-composites/src/composites/localization/locales/en-GB/strings.json @@ -409,4 +409,4 @@ "breakoutRoomChatPaneTitle": "Breakout Room Chat", "chatContentSpinnerLabel": "Loading..." } -} \ No newline at end of file +} diff --git a/packages/react-composites/src/composites/localization/locales/es-ES/strings.json b/packages/react-composites/src/composites/localization/locales/es-ES/strings.json index d470c5d2dff..24fa53d2630 100644 --- a/packages/react-composites/src/composites/localization/locales/es-ES/strings.json +++ b/packages/react-composites/src/composites/localization/locales/es-ES/strings.json @@ -409,4 +409,4 @@ "breakoutRoomChatPaneTitle": "Chat de sala para sesión de subgrupo", "chatContentSpinnerLabel": "Cargando..." } -} \ No newline at end of file +} diff --git a/packages/react-composites/src/composites/localization/locales/fi-FI/strings.json b/packages/react-composites/src/composites/localization/locales/fi-FI/strings.json index d68f2adda60..ac03bf2e000 100644 --- a/packages/react-composites/src/composites/localization/locales/fi-FI/strings.json +++ b/packages/react-composites/src/composites/localization/locales/fi-FI/strings.json @@ -409,4 +409,4 @@ "breakoutRoomChatPaneTitle": "Pienryhmätilan keskustelu", "chatContentSpinnerLabel": "Ladataan..." } -} \ No newline at end of file +} diff --git a/packages/react-composites/src/composites/localization/locales/fr-FR/strings.json b/packages/react-composites/src/composites/localization/locales/fr-FR/strings.json index 9546d4c0c2b..f7cc8dcdbe2 100644 --- a/packages/react-composites/src/composites/localization/locales/fr-FR/strings.json +++ b/packages/react-composites/src/composites/localization/locales/fr-FR/strings.json @@ -409,4 +409,4 @@ "breakoutRoomChatPaneTitle": "Conversation dans une salle pour petit groupe", "chatContentSpinnerLabel": "Chargement en cours..." } -} \ No newline at end of file +} diff --git a/packages/react-composites/src/composites/localization/locales/he-IL/strings.json b/packages/react-composites/src/composites/localization/locales/he-IL/strings.json index 816be55e9fa..82358060830 100644 --- a/packages/react-composites/src/composites/localization/locales/he-IL/strings.json +++ b/packages/react-composites/src/composites/localization/locales/he-IL/strings.json @@ -409,4 +409,4 @@ "breakoutRoomChatPaneTitle": "צ'אט פיצול חדרים", "chatContentSpinnerLabel": "בטעינה..." } -} \ No newline at end of file +} diff --git a/packages/react-composites/src/composites/localization/locales/it-IT/strings.json b/packages/react-composites/src/composites/localization/locales/it-IT/strings.json index 8a1f57e812a..41e13ea14a3 100644 --- a/packages/react-composites/src/composites/localization/locales/it-IT/strings.json +++ b/packages/react-composites/src/composites/localization/locales/it-IT/strings.json @@ -409,4 +409,4 @@ "breakoutRoomChatPaneTitle": "Chat della stanza di lavoro", "chatContentSpinnerLabel": "Caricamento in corso..." } -} \ No newline at end of file +} diff --git a/packages/react-composites/src/composites/localization/locales/ja-JP/strings.json b/packages/react-composites/src/composites/localization/locales/ja-JP/strings.json index 42d1be24fff..da650f6f371 100644 --- a/packages/react-composites/src/composites/localization/locales/ja-JP/strings.json +++ b/packages/react-composites/src/composites/localization/locales/ja-JP/strings.json @@ -409,4 +409,4 @@ "breakoutRoomChatPaneTitle": "ブレークアウト ルーム チャット", "chatContentSpinnerLabel": "読み込んでいます..." } -} \ No newline at end of file +} diff --git a/packages/react-composites/src/composites/localization/locales/ko-KR/strings.json b/packages/react-composites/src/composites/localization/locales/ko-KR/strings.json index 8d415ba7d12..372bfe9246c 100644 --- a/packages/react-composites/src/composites/localization/locales/ko-KR/strings.json +++ b/packages/react-composites/src/composites/localization/locales/ko-KR/strings.json @@ -409,4 +409,4 @@ "breakoutRoomChatPaneTitle": "소규모 회의실 채팅", "chatContentSpinnerLabel": "로드하는 중..." } -} \ No newline at end of file +} diff --git a/packages/react-composites/src/composites/localization/locales/nb-NO/strings.json b/packages/react-composites/src/composites/localization/locales/nb-NO/strings.json index 7f49bad1954..b68181e5196 100644 --- a/packages/react-composites/src/composites/localization/locales/nb-NO/strings.json +++ b/packages/react-composites/src/composites/localization/locales/nb-NO/strings.json @@ -409,4 +409,4 @@ "breakoutRoomChatPaneTitle": "Grupperomchat", "chatContentSpinnerLabel": "Laster inn ..." } -} \ No newline at end of file +} diff --git a/packages/react-composites/src/composites/localization/locales/nl-NL/strings.json b/packages/react-composites/src/composites/localization/locales/nl-NL/strings.json index 5f15cc3c1ab..b487387c5e8 100644 --- a/packages/react-composites/src/composites/localization/locales/nl-NL/strings.json +++ b/packages/react-composites/src/composites/localization/locales/nl-NL/strings.json @@ -409,4 +409,4 @@ "breakoutRoomChatPaneTitle": "Aparte vergaderruimte chat", "chatContentSpinnerLabel": "Laden..." } -} \ No newline at end of file +} diff --git a/packages/react-composites/src/composites/localization/locales/pl-PL/strings.json b/packages/react-composites/src/composites/localization/locales/pl-PL/strings.json index 77e5a3b2e84..4897d546a99 100644 --- a/packages/react-composites/src/composites/localization/locales/pl-PL/strings.json +++ b/packages/react-composites/src/composites/localization/locales/pl-PL/strings.json @@ -409,4 +409,4 @@ "breakoutRoomChatPaneTitle": "Czat osobnego pokoju", "chatContentSpinnerLabel": "Trwa ładowanie..." } -} \ No newline at end of file +} diff --git a/packages/react-composites/src/composites/localization/locales/pt-BR/strings.json b/packages/react-composites/src/composites/localization/locales/pt-BR/strings.json index dcca907709c..365772663c1 100644 --- a/packages/react-composites/src/composites/localization/locales/pt-BR/strings.json +++ b/packages/react-composites/src/composites/localization/locales/pt-BR/strings.json @@ -409,4 +409,4 @@ "breakoutRoomChatPaneTitle": "Chat da Sala para Sessão de Grupo", "chatContentSpinnerLabel": "Carregando..." } -} \ No newline at end of file +} diff --git a/packages/react-composites/src/composites/localization/locales/ru-RU/strings.json b/packages/react-composites/src/composites/localization/locales/ru-RU/strings.json index f7171c517ad..1ad167efb59 100644 --- a/packages/react-composites/src/composites/localization/locales/ru-RU/strings.json +++ b/packages/react-composites/src/composites/localization/locales/ru-RU/strings.json @@ -409,4 +409,4 @@ "breakoutRoomChatPaneTitle": "Чат переговорной комнаты", "chatContentSpinnerLabel": "Загрузка…" } -} \ No newline at end of file +} diff --git a/packages/react-composites/src/composites/localization/locales/sv-SE/strings.json b/packages/react-composites/src/composites/localization/locales/sv-SE/strings.json index 0aac7b25dce..d6d03babe9b 100644 --- a/packages/react-composites/src/composites/localization/locales/sv-SE/strings.json +++ b/packages/react-composites/src/composites/localization/locales/sv-SE/strings.json @@ -409,4 +409,4 @@ "breakoutRoomChatPaneTitle": "Grupprumschatt", "chatContentSpinnerLabel": "Läser in ..." } -} \ No newline at end of file +} diff --git a/packages/react-composites/src/composites/localization/locales/tr-TR/strings.json b/packages/react-composites/src/composites/localization/locales/tr-TR/strings.json index 4dd9a5a25b5..fd28613cf05 100644 --- a/packages/react-composites/src/composites/localization/locales/tr-TR/strings.json +++ b/packages/react-composites/src/composites/localization/locales/tr-TR/strings.json @@ -409,4 +409,4 @@ "breakoutRoomChatPaneTitle": "Tartışma Odası Sohbeti", "chatContentSpinnerLabel": "Yükleniyor..." } -} \ No newline at end of file +} diff --git a/packages/react-composites/src/composites/localization/locales/zh-CN/strings.json b/packages/react-composites/src/composites/localization/locales/zh-CN/strings.json index d27bbcf8caa..a19f6b873db 100644 --- a/packages/react-composites/src/composites/localization/locales/zh-CN/strings.json +++ b/packages/react-composites/src/composites/localization/locales/zh-CN/strings.json @@ -409,4 +409,4 @@ "breakoutRoomChatPaneTitle": "分组讨论室聊天", "chatContentSpinnerLabel": "正在加载..." } -} \ No newline at end of file +} diff --git a/packages/react-composites/src/composites/localization/locales/zh-TW/strings.json b/packages/react-composites/src/composites/localization/locales/zh-TW/strings.json index fbab13452dd..bab4c294d28 100644 --- a/packages/react-composites/src/composites/localization/locales/zh-TW/strings.json +++ b/packages/react-composites/src/composites/localization/locales/zh-TW/strings.json @@ -409,4 +409,4 @@ "breakoutRoomChatPaneTitle": "分組討論區聊天", "chatContentSpinnerLabel": "正在載入..." } -} \ No newline at end of file +} diff --git a/packages/react-composites/tests/app/lib/MockCallAdapter.ts b/packages/react-composites/tests/app/lib/MockCallAdapter.ts index 90c696b3b52..7203fcb17b4 100644 --- a/packages/react-composites/tests/app/lib/MockCallAdapter.ts +++ b/packages/react-composites/tests/app/lib/MockCallAdapter.ts @@ -91,6 +91,12 @@ export class MockCallAdapter implements CallAdapter { disposeStreamView(): Promise { throw Error('disposeStreamView not implemented'); } + createTogetherModeStreamViews(): Promise { + throw Error('createFeatureStreamView not implemented'); + } + disposeTogetherModeStreamViews(): Promise { + throw Error('disposeFeatureStreamView not implemented'); + } disposeScreenShareStreamView(): Promise { return Promise.resolve(); } diff --git a/packages/react-composites/tests/browser/call/hermetic/fixture.ts b/packages/react-composites/tests/browser/call/hermetic/fixture.ts index 91920c74847..ecd2a039b8f 100644 --- a/packages/react-composites/tests/browser/call/hermetic/fixture.ts +++ b/packages/react-composites/tests/browser/call/hermetic/fixture.ts @@ -92,7 +92,7 @@ export function defaultMockCallAdapterState( remoteParticipantsEnded: {}, raiseHand: { raisedHands: [] }, /* @conditional-compile-remove(together-mode) */ - togetherMode: { stream: [] }, + togetherMode: { streams: new Map(), seatingCoordinates: new Map() }, pptLive: { isActive: false }, role: role ?? 'Unknown', dominantSpeakers: dominantSpeakers, @@ -537,7 +537,7 @@ const defaultEndedCallState: CallState = { remoteParticipantsEnded: {}, raiseHand: { raisedHands: [] }, /* @conditional-compile-remove(together-mode) */ - togetherMode: { stream: [] }, + togetherMode: { streams: new Map(), seatingCoordinates: new Map() }, pptLive: { isActive: false }, captionsFeature: { captions: [],