UNPKG

@azure/communication-react

Version:

React library for building modern communication user experiences utilizing Azure Communication Services

395 lines • 21.8 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { LocalVideoStream, VideoStreamRenderer } from '@azure/communication-calling'; import { convertSdkLocalStreamToDeclarativeLocalStream, convertSdkRemoteStreamToDeclarativeRemoteStream, convertFromSDKToDeclarativeVideoStreamRendererView } from './Converter'; import { toFlatCommunicationIdentifier } from "../../acs-ui-common/src"; import { EventNames } from './Logger'; import { _logStreamEvent } from './StreamUtilsLogging'; function createViewVideo(context, internalContext, callId, stream, participantId, options) { return __awaiter(this, void 0, void 0, function* () { // we can only have 3 types of createView let streamEventType; // we will reuse these for local as well but we need to make sure the remote stream is passed in like before. if (participantId) { streamEventType = 'createViewRemote'; } else if (callId) { streamEventType = 'createViewLocal'; } else { // TODO update for when unparented view. throw new Error('unparented createView not implemented yet here'); streamEventType = 'createViewUnparented'; } const streamType = stream === null || stream === void 0 ? void 0 : stream.mediaStreamType; const localStreamKey = stream.mediaStreamType; const remoteStreamId = stream.id; // we want to check to see if there is a participantId this will tell us whether its a local stream or a remote one. const participantKey = streamEventType === 'createViewRemote' && participantId ? typeof participantId === 'string' ? participantId : toFlatCommunicationIdentifier(participantId) : undefined; const streamLogInfo = { callId, participantKey, streamId: remoteStreamId !== null && remoteStreamId !== void 0 ? remoteStreamId : localStreamKey, streamType, streamEventType }; // make different logging announcement based on whether or not we are starting a local or remote _logStreamEvent(EventNames.CREATING_VIEW, streamLogInfo); // if we have a participant Id and a stream get the remote info, else get the local render info from state. const renderInfo = streamEventType === 'createViewRemote' && participantKey ? internalContext.getRemoteRenderInfoForParticipant(callId, participantKey, remoteStreamId) : internalContext.getLocalRenderInfo(callId, localStreamKey); 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') { if (streamEventType === 'createViewRemote' && participantKey) { _logStreamEvent(EventNames.STREAM_STOPPING, streamLogInfo); internalContext.setRemoteRenderInfo(callId, participantKey, remoteStreamId, renderInfo.stream, 'Rendering', renderInfo.renderer); } else if (streamEventType === 'createViewLocal') { _logStreamEvent(EventNames.STREAM_STOPPING, streamLogInfo); internalContext.setLocalRenderInfo(callId, localStreamKey, renderInfo.stream, 'Rendering', renderInfo.renderer); } return; } const renderer = new VideoStreamRenderer(renderInfo.stream); if (streamEventType === 'createViewRemote' && participantKey) { internalContext.setRemoteRenderInfo(callId, participantKey, remoteStreamId, renderInfo.stream, 'Rendering', undefined); } else { internalContext.setLocalRenderInfo(callId, localStreamKey, renderInfo.stream, 'Rendering', renderer); } let view; try { view = yield renderer.createView(options); } catch (e) { if (streamEventType === 'createViewRemote' && participantKey) { _logStreamEvent(EventNames.CREATE_STREAM_FAIL, streamLogInfo); internalContext.setRemoteRenderInfo(callId, participantKey, remoteStreamId, renderInfo.stream, 'NotRendered', undefined); } else if (streamEventType === 'createViewLocal') { _logStreamEvent(EventNames.CREATE_STREAM_FAIL, streamLogInfo, e); internalContext.setLocalRenderInfo(callId, localStreamKey, 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 = streamEventType === 'createViewRemote' && participantKey ? internalContext.getRemoteRenderInfoForParticipant(callId, participantKey, remoteStreamId) : internalContext.getLocalRenderInfo(callId, localStreamKey); 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(); if (streamEventType === 'createViewRemote' && participantKey) { context.setRemoteVideoStreamRendererView(callId, participantKey, remoteStreamId, undefined); } else { context.setLocalVideoStreamRendererView(callId, localStreamKey, 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(); if (streamEventType === 'createViewRemote' && participantKey) { internalContext.setRemoteRenderInfo(callId, participantKey, remoteStreamId, refreshedRenderInfo.stream, 'NotRendered', undefined); context.setRemoteVideoStreamRendererView(callId, participantKey, remoteStreamId, undefined); } else if (streamEventType === 'createViewLocal') { internalContext.setLocalRenderInfo(callId, localStreamKey, refreshedRenderInfo.stream, 'NotRendered', undefined); context.setLocalVideoStreamRendererView(callId, localStreamKey, undefined); } return; } // Else the stream still exists and status is not telling us to stop rendering. Complete the render process by // updating the state. if (streamEventType === 'createViewRemote' && participantKey) { internalContext.setRemoteRenderInfo(callId, participantKey, remoteStreamId, refreshedRenderInfo.stream, 'Rendered', renderer); context.setRemoteVideoStreamRendererView(callId, participantKey, remoteStreamId, convertFromSDKToDeclarativeVideoStreamRendererView(view)); _logStreamEvent(EventNames.VIEW_RENDER_SUCCEED, streamLogInfo); } else if (streamEventType === 'createViewLocal') { internalContext.setLocalRenderInfo(callId, localStreamKey, refreshedRenderInfo.stream, 'Rendered', renderer); context.setLocalVideoStreamRendererView(callId, localStreamKey, convertFromSDKToDeclarativeVideoStreamRendererView(view)); _logStreamEvent(EventNames.VIEW_RENDER_SUCCEED, streamLogInfo); } return { renderer, view }; }); } function createViewUnparentedVideo(context, internalContext, stream, options) { return __awaiter(this, void 0, void 0, function* () { const renderInfo = internalContext.getUnparentedRenderInfo(stream); if (renderInfo && renderInfo.status === 'Rendered') { console.warn('Unparented LocalVideoStream is already rendered'); return; } if (renderInfo && 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. return; } if (renderInfo && renderInfo.status === 'Stopping') { console.warn('Unparented LocalVideoStream is in the middle of stopping'); return; } const localVideoStream = new LocalVideoStream(stream.source); const renderer = new VideoStreamRenderer(localVideoStream); internalContext.setUnparentedRenderInfo(stream, localVideoStream, 'Rendering', undefined); let view; try { view = yield renderer.createView(options); } catch (e) { // Special case for unparented views. Since they are not tied to anything and created by us based on the calls to // this function we'll delete it to clean up the data since keeping it around doesn't help us and if developer wants // to create a new view they can check that the view is not rendered and call this function again. internalContext.deleteUnparentedRenderInfo(stream); 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.getUnparentedRenderInfo(stream); if (!refreshedRenderInfo) { // Unparented stream's RenderInfo was deleted. Currently this shouldn't happen but if it does we'll just dispose the // renderer and clean up state. If developer wanted the stream they could call this function again and that should // generate new working state via this function. renderer.dispose(); context.deleteDeviceManagerUnparentedView(stream); 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. Special case for unparented views, delete them from state when stopped to free up // the memory since we were the ones generating this and not tied to any Call state. internalContext.deleteUnparentedRenderInfo(stream); context.deleteDeviceManagerUnparentedView(stream); return; } // Else the stream still exists and status is not telling us to stop rendering. Complete the render process by // updating the state. internalContext.setUnparentedRenderInfo(stream, localVideoStream, 'Rendered', renderer); internalContext.subscribeToUnparentedViewVideoEffects(localVideoStream, context); context.setDeviceManagerUnparentedView(stream, convertFromSDKToDeclarativeVideoStreamRendererView(view)); return { renderer, view }; }); } function disposeViewVideo(context, internalContext, callId, stream, participantId) { // we can only have 3 types of createView let streamEventType; // we will reuse these for local as well but we need to make sure the remote stream is passed in like before. if (participantId) { streamEventType = 'disposeViewRemote'; } else if (callId) { streamEventType = 'disposeViewLocal'; } else { // TODO update for when unparented view. streamEventType = 'disposeViewUnparented'; } const streamType = stream.mediaStreamType; const localStreamKey = stream.mediaStreamType; const remoteStreamId = stream.id; // we want to check to see if there is a participantId this will tell us whether its a local stream or a remote one. const participantKey = streamEventType === 'disposeViewRemote' && participantId ? typeof participantId === 'string' ? participantId : toFlatCommunicationIdentifier(participantId) : undefined; const streamLogInfo = { callId, participantKey, streamId: remoteStreamId !== null && remoteStreamId !== void 0 ? remoteStreamId : localStreamKey, streamType }; _logStreamEvent(EventNames.START_DISPOSE_STREAM, streamLogInfo); if (streamEventType === 'disposeViewRemote' && participantKey) { context.setRemoteVideoStreamRendererView(callId, participantKey, remoteStreamId, undefined); } const renderInfo = streamEventType === 'disposeViewRemote' && participantKey ? internalContext.getRemoteRenderInfoForParticipant(callId, participantKey, remoteStreamId) : internalContext.getLocalRenderInfo(callId, localStreamKey); 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); if (streamEventType === 'disposeViewRemote' && participantKey) { internalContext.setRemoteRenderInfo(callId, participantKey, remoteStreamId, renderInfo.stream, 'Stopping', undefined); } else { internalContext.setLocalRenderInfo(callId, localStreamKey, 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. if (streamEventType === 'disposeViewRemote' && participantKey) { internalContext.setRemoteRenderInfo(callId, participantKey, remoteStreamId, renderInfo.stream, 'NotRendered', undefined); } else if (streamEventType === 'disposeViewLocal') { internalContext.setLocalRenderInfo(callId, localStreamKey, renderInfo.stream, 'NotRendered', undefined); context.setLocalVideoStreamRendererView(callId, localStreamKey, undefined); } } else { _logStreamEvent(EventNames.RENDERER_NOT_FOUND, streamLogInfo); } } function disposeViewUnparentedVideo(context, internalContext, stream) { const streamType = stream.mediaStreamType; const streamLogInfo = { streamType, streamEventType: 'disposeViewUnparented' }; _logStreamEvent(EventNames.START_DISPOSE_STREAM, streamLogInfo); context.deleteDeviceManagerUnparentedView(stream); const renderInfo = internalContext.getUnparentedRenderInfo(stream); if (!renderInfo) { _logStreamEvent(EventNames.DISPOSE_INFO_NOT_FOUND, streamLogInfo); return; } if (renderInfo.status === 'Rendering') { _logStreamEvent(EventNames.STREAM_STOPPING, streamLogInfo); internalContext.setUnparentedRenderInfo(stream, renderInfo.stream, 'Stopping', undefined); } else { internalContext.deleteUnparentedRenderInfo(stream); } if (renderInfo.renderer) { _logStreamEvent(EventNames.DISPOSING_RENDERER, streamLogInfo); renderInfo.renderer.dispose(); } else { _logStreamEvent(EventNames.RENDERER_NOT_FOUND, streamLogInfo); } } /** * @private */ export function createView(context, internalContext, callId, participantId, stream, options) { const streamType = stream.mediaStreamType; if (callId) { return createViewVideo(context, internalContext, callId, stream, participantId, options); } else if (!('id' in stream) && !callId) { // Render LocalVideoStream that is not part of a Call // Because it is not part of the call we don't tee errors to state naturally (e.g. via a Call Client function such as startVideo). // We do not have a startLocalPreviewVideo function, so as a workaround we ensure any errors are propagated here. return context.withAsyncErrorTeedToState(() => __awaiter(this, void 0, void 0, function* () { return yield createViewUnparentedVideo(context, internalContext, stream, options); }), 'Call.startVideo')(); } else { _logStreamEvent(EventNames.CREATE_STREAM_INVALID_PARAMS, { streamType }); return Promise.resolve(undefined); } } /** * @private */ export function disposeView(context, internalContext, callId, participantId, stream) { const streamType = stream.mediaStreamType; if (callId) { disposeViewVideo(context, internalContext, callId, stream, participantId); } else if (!('id' in stream) && !callId) { // Stop rendering LocalVideoStream that is not part of a Call // Because it is not part of the call we don't tee errors to state naturally (e.g. via a Call Client function such as startVideo). // We do not have a stopLocalPreviewVideo function, so as a workaround we ensure any errors are propagated here. context.withErrorTeedToState(() => disposeViewUnparentedVideo(context, internalContext, stream), 'Call.stopVideo')(); } else { _logStreamEvent(EventNames.DISPOSE_STREAM_INVALID_PARAMS, { streamType }); return; } } /** * @private * Only stops videos that are tied to a Call. */ export function disposeAllViewsFromCall(context, internalContext, callId) { const remoteStreams = internalContext.getRemoteRenderInfoForCall(callId); if (remoteStreams) { for (const [participantKey, participantStreams] of remoteStreams.entries()) { for (const [_, remoteStreamAndRenderer] of participantStreams.entries()) { // We don't want to accept SDK stream as parameter but we also don't cache the declarative stream so we have to // convert the SDK stream to declarative stream which is not pretty so this could use some further refactoring. disposeView(context, internalContext, callId, participantKey, convertSdkRemoteStreamToDeclarativeRemoteStream(remoteStreamAndRenderer.stream)); } } } const localStreams = internalContext.getLocalRenderInfosForCall(callId); if (localStreams) { for (const localStreamAndRenderer of localStreams.values()) { if (localStreamAndRenderer && localStreamAndRenderer.renderer) { // We don't want to accept SDK stream as parameter but we also don't cache the declarative stream so we have to // convert the SDK stream to declarative stream which is not pretty so this could use some further refactoring. disposeView(context, internalContext, callId, undefined, convertSdkLocalStreamToDeclarativeLocalStream(localStreamAndRenderer.stream)); } } } const callFeatureStreams = internalContext.getCallFeatureRenderInfosForCall(callId); if (callFeatureStreams) { for (const featureStreams of callFeatureStreams.values()) { for (const streamAndRenderer of featureStreams.values()) { disposeView(context, internalContext, callId, undefined, convertSdkRemoteStreamToDeclarativeRemoteStream(streamAndRenderer.stream)); } } } } /** * @private */ export function disposeAllViews(context, internalContext) { const callIds = internalContext.getCallIds(); for (const callId of callIds) { disposeAllViewsFromCall(context, internalContext, callId); } } //# sourceMappingURL=StreamUtils.js.map