@techsee/techsee-media-service 0.23.0 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -15,7 +15,7 @@ export declare enum MULTI_PARTY_EVENTS {
|
|
|
15
15
|
participantsNumberChanged = "participantsNumberChanged"
|
|
16
16
|
}
|
|
17
17
|
export declare type Subscriber = {
|
|
18
|
-
subscriber:
|
|
18
|
+
subscriber: unknown;
|
|
19
19
|
connectionId: string;
|
|
20
20
|
clientRole: SessionClientRole | 'default';
|
|
21
21
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/MultiParty/MultiParty.ts"],"names":[],"mappings":";AAEA,OAAO,EAAC,YAAY,EAAC,MAAM,QAAQ,CAAC;AACpC,OAAO,EAAC,gBAAgB,EAAC,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAC,iBAAiB,EAAC,MAAM,mBAAmB,CAAC;AAIpD,eAAO,MAAM,gBAAgB,gBAAgB,CAAC;AAC9C,eAAO,MAAM,sBAAsB,uBAAuB,CAAC;AAC3D,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC,oBAAY,kBAAkB;IAC1B,aAAa,kBAAkB;IAC/B,eAAe,oBAAoB;IACnC,iBAAiB,sBAAsB;IACvC,mBAAmB,wBAAwB;IAC3C,aAAa,kBAAkB;IAC/B,mBAAmB,wBAAwB;IAC3C,yBAAyB,8BAA8B;CAC1D;AAED,oBAAY,UAAU,GAAG;IACrB,UAAU,EAAE,
|
|
1
|
+
{"version":3,"sources":["../src/MultiParty/MultiParty.ts"],"names":[],"mappings":";AAEA,OAAO,EAAC,YAAY,EAAC,MAAM,QAAQ,CAAC;AACpC,OAAO,EAAC,gBAAgB,EAAC,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAC,iBAAiB,EAAC,MAAM,mBAAmB,CAAC;AAIpD,eAAO,MAAM,gBAAgB,gBAAgB,CAAC;AAC9C,eAAO,MAAM,sBAAsB,uBAAuB,CAAC;AAC3D,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC,oBAAY,kBAAkB;IAC1B,aAAa,kBAAkB;IAC/B,eAAe,oBAAoB;IACnC,iBAAiB,sBAAsB;IACvC,mBAAmB,wBAAwB;IAC3C,aAAa,kBAAkB;IAC/B,mBAAmB,wBAAwB;IAC3C,yBAAyB,8BAA8B;CAC1D;AAED,oBAAY,UAAU,GAAG;IACrB,UAAU,EAAE,OAAO,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,iBAAiB,GAAG,SAAS,CAAC;CAC7C,CAAC;AAEF,MAAM,WAAW,WAAW;IACxB,OAAO,CAAC,EAAE,GAAG,CAAC;IACd,WAAW,CAAC,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;IAEhC,OAAO,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC9B,UAAU,IAAI,IAAI,CAAC;IACnB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,WAAW,CAAC;CAC/E;AAED,8BAAsB,UAAW,SAAQ,YAAa,YAAW,WAAW;IACxE,OAAO,CAAC,EAAE,GAAG,CAAC;IACd,QAAQ,CAAC,WAAW,CAAC,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;IACzC,SAAS,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IAErC,QAAQ,CAAC,OAAO,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI;IACtC,QAAQ,CAAC,UAAU,IAAI,IAAI;IAC3B,QAAQ,CAAC,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;gBAE1B,QAAQ,EAAE,gBAAgB;CAIzC;AAED,eAAO,MAAM,oBAAoB,QAAa,QAAQ,OAAO,CAW5D,CAAC","file":"MultiParty.d.ts","sourcesContent":["import {MediaCapabilitiesHelper} from './MediaCapabilitiesHelper';\nimport {getMediaTracer} from '../MediaUtils/MediaTracer';\nimport {EventEmitter} from 'events';\nimport {ISessionSettings} from './Contracts';\nimport {SessionClientRole} from '../MediaConstants';\n\nconst trace = getMediaTracer('Multiparty');\n\nexport const participantDomId = 'participant';\nexport const pinnedParticipantDomId = 'pinned-participant';\nexport const maxParticipantsNumber = 2;\n\nexport enum MULTI_PARTY_EVENTS {\n connectFailed = 'connectFailed',\n streamDestroyed = 'streamDestroyed',\n connectionCreated = 'connectionCreated',\n connectionDestroyed = 'connectionDestroyed',\n streamCreated = 'streamCreated',\n videoElementCreated = 'videoElementCreated',\n participantsNumberChanged = 'participantsNumberChanged'\n}\n\nexport type Subscriber = {\n subscriber: unknown;\n connectionId: string;\n clientRole: SessionClientRole | 'default';\n};\n\nexport interface IMultiParty {\n session?: any;\n subscribers?: Array<Subscriber>;\n\n connect(...args: any[]): void;\n disconnect(): void;\n switchCamera(): Promise<void>;\n on(event: string | symbol, listener: (...args: any[]) => void): IMultiParty;\n}\n\nexport abstract class MultiParty extends EventEmitter implements IMultiParty {\n session?: any;\n abstract subscribers?: Array<Subscriber>;\n protected settings: ISessionSettings;\n\n abstract connect(...args: any[]): void;\n abstract disconnect(): void;\n abstract switchCamera(): Promise<void>;\n\n constructor(settings: ISessionSettings) {\n super();\n this.settings = settings;\n }\n}\n\nexport const couldStartMultiparty = async (): Promise<boolean> => {\n try {\n const mediaCapabilitiesHelper = await MediaCapabilitiesHelper.init();\n\n return mediaCapabilitiesHelper.hasWebRTC();\n\n } catch (err) {\n trace.error(JSON.stringify(err));\n\n return false;\n }\n};\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/MultiParty/OpentokMultiparty.ts"],"names":[],"mappings":"AACA,OAAO,EAA4C,UAAU,EAA4C,UAAU,EAAC,MAAM,cAAc,CAAC;AAIzI,OAAO,EAAC,YAAY,EAAE,gBAAgB,EAAC,MAAM,aAAa,CAAC;AAK3D,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAUjC,qBAAa,iBAAkB,SAAQ,UAAU;IAC7C,WAAW,EAAE,KAAK,CAAC,UAAU,CAAC,CAAM;IACpC,OAAO,CAAC,EAAE,EAAE,CAAC,OAAO,GAAG,IAAI,CAAC;IAC5B,gBAAgB,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC;IAEhC,gDAAgD;IAChD,WAAW,CAAC,EAAE,YAAY,CAAC;IAC3B,gBAAgB,SAAK;IAErB,IAAI,eAAe,IAAI,MAAM,GAAG,SAAS,CAExC;IAED,OAAO,gBAAiB,YAAY,KAAG,IAAI,CAmCzC;IAEF,UAAU,QAAO,IAAI,CAYnB;IAEF,sBAAsB,aAAoB,gBAAgB,KAAG,QAAQ,OAAO,GAAG,KAAK,CAAC,CAYnF;IAEF,sBAAsB,aAAoB,gBAAgB,KAAG,QAAQ,OAAO,GAAG,KAAK,CAAC,CASnF;IAEF,eAAe,yBAAgC,gBAAgB,KAAG,QAAQ,IAAI,CAAC,CA2B7E;IAEF,YAAY,UAAiB,OAAO,KAAG,QAAQ,IAAI,CAAC,CAgBlD;IAEF,YAAY,UAAiB,OAAO,KAAG,QAAQ,IAAI,CAAC,CAgBlD;IAEF,YAAY,QAAa,QAAQ,IAAI,CAAC,CAiCpC;IAEF,uEAAuE;IACvE,OAAO,CAAC,aAAa;IAIrB,aAAa,aAAc,gBAAgB,KAAG,IAAI,CAiChD;IAEF,oBAAoB;gBACR,GAAG,MAAM;UACjB,IAAI,CAuCN;IAEF,sBAAsB;gBACV,GAAG,MAAM;gBACT,MAAM;UACd,IAAI,CAON;IAEF,wBAAwB;oBACR,GAAG,UAAU;UACzB,IAAI,CAaN;IAEF,0BAA0B;oBACV,GAAG,UAAU;gBACjB,MAAM;UACd,IAAI,CAYN;IAEF,kBAAkB,iCAAoB,IAAI,CAExC;CACL","file":"OpentokMultiparty.d.ts","sourcesContent":["\nimport {maxParticipantsNumber, MULTI_PARTY_EVENTS, MultiParty, participantDomId, pinnedParticipantDomId, Subscriber} from './MultiParty';\nimport {CameraTypes, SessionClientRole} from '../MediaConstants';\nimport {getMediaTracer} from '../MediaUtils/MediaTracer';\nimport get from 'lodash/get';\nimport {ICredentials, ISessionSettings} from './Contracts';\nimport filter from 'lodash/filter';\nimport cloneDeep from 'lodash/cloneDeep';\nimport {MediaCapabilitiesHelper} from './MediaCapabilitiesHelper';\n\nimport OT from '@opentok/client';\nconst trace = getMediaTracer('Multiparty');\n\n// The preferred camera position to use for the video source\nenum OpentokFacingMode {\n USER = 'user',\n ENVIRONMENT = 'environment'\n}\n\n\nexport class OpentokMultiparty extends MultiParty {\n subscribers: Array<Subscriber> = [];\n session?: OT.Session | null;\n currentPublisher?: OT.Publisher;\n\n /** @FIXME not used atm, should be refactored */\n credentials?: ICredentials;\n participantCount = 0;\n\n get ownConnectionId(): string | undefined {\n return this.session?.connection?.connectionId;\n }\n\n connect = (credentials: ICredentials): void => {\n this.credentials = credentials;\n\n try {\n this.session = OT.initSession(credentials.apiKey, credentials.sessionId);\n } catch (err) {\n trace.error(JSON.stringify(err));\n this.emit(MULTI_PARTY_EVENTS.connectFailed, {meta: {err: JSON.stringify(err)}});\n\n return;\n }\n\n this.session.on('connectionCreated', this.connectionCreatedHandler);\n this.session.on('connectionDestroyed', this.connectionDestroyedHandler);\n this.session.on('streamCreated', this.streamCreatedHandler);\n this.session.on('streamDestroyed', this.streamDestroyedHandler);\n\n this.session.connect(credentials.token, async (error?: OT.OTError) => {\n if (error) {\n this.session?.off();\n this.session = null;\n this.traceErrorCallback(error);\n this.emit(MULTI_PARTY_EVENTS.connectFailed, {meta: {err: JSON.stringify(error)}});\n\n return;\n }\n\n if (this.settings.defaultCamera === CameraTypes.NONE && this.settings.startWithAudioMuted) {\n trace.info('camera is none, no need to create publisher');\n\n return;\n }\n\n await this.createPublisher(this.settings);\n });\n };\n\n disconnect = (): void => {\n try {\n trace.info('Disconnecting from session');\n\n if (this.session) {\n this.session.disconnect();\n this.participantCount = 0;\n trace.info('Disconnection from session completed');\n }\n } catch (err) {\n trace.error(JSON.stringify(err));\n }\n };\n\n couldStartPublishVideo = async (settings: ISessionSettings): Promise<boolean | Error> => {\n try {\n const mediaCapabilitiesHelper = await MediaCapabilitiesHelper.init();\n\n return (settings.defaultCamera !== CameraTypes.NONE)\n ? mediaCapabilitiesHelper.hasCamera()\n : false;\n\n } catch (err) {\n trace.error(JSON.stringify(err));\n throw err;\n }\n };\n\n couldStartPublishAudio = async (settings: ISessionSettings): Promise<boolean | Error> => {\n try {\n const mediaCapabilitiesHelper = await MediaCapabilitiesHelper.init();\n\n return !settings.startWithAudioMuted && mediaCapabilitiesHelper.hasMicrophone();\n } catch (err) {\n trace.error(JSON.stringify(err));\n throw err;\n }\n };\n\n createPublisher = async (settingsForPublisher: ISessionSettings): Promise<void> => {\n const settings = cloneDeep(settingsForPublisher);\n const couldStartPublishVideo = await this.couldStartPublishVideo(settings);\n const couldStartPublishAudio = await this.couldStartPublishAudio(settings);\n\n if (!couldStartPublishVideo && !couldStartPublishAudio) {\n // FIXME: use emitter\n trace.error('could not create a publisher, due to lack of media capabilities or settings');\n\n return;\n }\n\n if (!couldStartPublishVideo) {\n settings.defaultCamera = CameraTypes.NONE;\n }\n\n if (!couldStartPublishAudio) {\n settings.startWithAudioMuted = true;\n }\n\n if (!this.currentPublisher) {\n this.initPublisher(settings);\n if (!this.currentPublisher) {\n throw new Error('Creating session requires initialized publisher');\n }\n this.currentPublisher && this.session?.publish(this.currentPublisher, this.traceErrorCallback);\n }\n };\n\n publishAudio = async (value: boolean): Promise<void> => {\n const settings = cloneDeep(this.settings);\n\n settings.startWithAudioMuted = !value;\n\n if (value && !await this.couldStartPublishAudio(settings)) {\n trace.error('could not publish audio');\n\n return;\n }\n\n if (!this.currentPublisher) {\n await this.createPublisher(settings);\n } else {\n this.currentPublisher.publishAudio(value);\n }\n };\n\n publishVideo = async (value: boolean): Promise<void> => {\n const settings = cloneDeep(this.settings);\n\n settings.defaultCamera = value ? this.settings.defaultCamera : CameraTypes.NONE;\n\n if (value && !await this.couldStartPublishVideo(settings)) {\n trace.error('could not publish video');\n\n return;\n }\n\n if (!this.currentPublisher) {\n await this.createPublisher(settings);\n } else {\n this.currentPublisher.publishVideo(value);\n }\n };\n\n switchCamera = async (): Promise<void> => {\n try {\n const mediaCapabilitiesHelper = await MediaCapabilitiesHelper.init();\n\n if (!mediaCapabilitiesHelper.supportSwitchCamera) {\n throw new Error('Not Support switch camera');\n }\n\n let deviceId = '';\n let cameraType = CameraTypes.NONE;\n\n switch (this.settings.defaultCamera) {\n case CameraTypes.BACK:\n cameraType = CameraTypes.FRONT;\n deviceId = mediaCapabilitiesHelper.frontCameraDeviceId;\n break;\n case CameraTypes.FRONT:\n cameraType = CameraTypes.BACK;\n deviceId = mediaCapabilitiesHelper.backCameraDeviceId;\n break;\n default:\n throw new Error(`cameraType not valid: ${this.settings.defaultCamera}`);\n }\n\n return this.currentPublisher?.setVideoSource(deviceId)\n .then(() => {\n this.settings.defaultCamera = cameraType;\n trace.info(`Success to switch camera to cameraType: ${cameraType}`);\n });\n } catch (err) {\n trace.error(JSON.stringify(err));\n throw err;\n }\n };\n\n /** @FIXME 'default' role is a temporary workaround for BE/Storybook */\n private getClientRole(connection?: OT.Connection): SessionClientRole | 'default' {\n return (connection?.data && JSON.parse(connection.data).clientRole) || 'default';\n }\n\n initPublisher = (settings: ISessionSettings): void => {\n const properties: OT.PublisherProperties = {\n insertMode: 'append',\n fitMode: 'cover',\n width: '100%',\n height: '100%',\n publishAudio: !settings.startWithAudioMuted,\n publishVideo: settings.defaultCamera !== CameraTypes.NONE,\n facingMode: settings.defaultCamera === CameraTypes.FRONT ? OpentokFacingMode.USER : OpentokFacingMode.ENVIRONMENT\n };\n\n const clientRole = this.getClientRole(this.session?.connection);\n const clientDiv = (clientRole === SessionClientRole.AGENT || clientRole === 'default')\n ? pinnedParticipantDomId\n : `${participantDomId}${this.participantCount}`;\n\n this.currentPublisher = OT.initPublisher(clientDiv, properties, this.traceErrorCallback);\n\n if (!this.currentPublisher) {\n trace.error('failed to init publisher');\n } else {\n this.currentPublisher.on('videoElementCreated', (event: any) => {\n trace.info('videoElementCreated', event);\n this.emit(MULTI_PARTY_EVENTS.videoElementCreated, event);\n });\n this.currentPublisher.on('streamDestroyed', (event: any) => {\n trace.info('Publisher stream destroyed', event);\n });\n this.currentPublisher.on('streamCreated', (event: any) => {\n this.emit(MULTI_PARTY_EVENTS.streamCreated, event);\n trace.info('publisher stream created', event, get(this.currentPublisher, 'stream.hasAudio'));\n });\n }\n };\n\n streamCreatedHandler = (event: OT.Event<'streamCreated', OT.Session> & {\n stream: OT.Stream;\n }): void => {\n trace.info('streamCreatedHandler', event.stream);\n this.emit(MULTI_PARTY_EVENTS.streamCreated, event);\n\n const {stream} = event;\n const {connection} = stream || {connection: null};\n\n if (!this.ownConnectionId || !stream || connection.connectionId === this.ownConnectionId) {\n return;\n }\n\n if (this.subscribers.length >= maxParticipantsNumber - 1) {\n trace.info(`a new subscriber cannot be added, as the number of participants reached its limit: ${maxParticipantsNumber}`);\n } else {\n // eslint-disable-next-line no-constant-condition\n const clientDiv = (this.getClientRole(connection) === SessionClientRole.AGENT)\n ? pinnedParticipantDomId\n : `${participantDomId}${this.participantCount}`;\n const properties: OT.SubscriberProperties = {\n insertMode: 'append',\n fitMode: 'cover',\n width: '100%',\n height: '100%'\n };\n\n const subscriber = event.target.subscribe(event.stream, clientDiv, properties, (err: any) => {\n this.traceErrorCallback(err);\n\n subscriber?.on('videoElementCreated', (event: any) => {\n trace.info('videoElementCreated', event);\n });\n });\n\n this.subscribers.push({\n subscriber,\n connectionId: connection.connectionId,\n clientRole: this.getClientRole(connection)\n });\n }\n };\n\n streamDestroyedHandler = (event: OT.Event<'streamDestroyed', OT.Session> & {\n stream: OT.Stream;\n reason: string;\n }): void => {\n trace.info('streamDestroyedHandler', event);\n const {stream} = event;\n const destroyedConnectionId = stream && stream.connection && stream.connection.connectionId;\n\n this.emit(MULTI_PARTY_EVENTS.streamDestroyed, {meta: {event: JSON.stringify(event)}});\n this.subscribers = filter(this.subscribers, (subscriber) => subscriber.connectionId !== destroyedConnectionId);\n };\n\n connectionCreatedHandler = (event: OT.Event<'connectionCreated', OT.Session> & {\n connection: OT.Connection;\n }): void => {\n trace.info('connectionCreatedHandler', event);\n const {connection} = event;\n\n if (!this.ownConnectionId || !connection) {\n return;\n }\n\n ++this.participantCount;\n this.emit(MULTI_PARTY_EVENTS.participantsNumberChanged, this.participantCount);\n const clientRole = this.getClientRole(connection);\n\n this.emit(MULTI_PARTY_EVENTS.connectionCreated, {meta: {clientRole, event: JSON.stringify(event)}});\n };\n\n connectionDestroyedHandler = (event: OT.Event<'connectionDestroyed', OT.Session> & {\n connection: OT.Connection;\n reason: string;\n }): void => {\n const {connection} = event;\n\n if (!this.ownConnectionId || !connection || connection.connectionId !== this.ownConnectionId) {\n return;\n }\n\n --this.participantCount;\n this.emit(MULTI_PARTY_EVENTS.participantsNumberChanged, this.participantCount);\n this.session = null;\n trace.info('connectionDestroyedHandler', event);\n this.emit(MULTI_PARTY_EVENTS.connectionDestroyed, {meta: {event: JSON.stringify(event)}});\n };\n\n traceErrorCallback = (error?: Error): void => {\n trace.error(JSON.stringify(error));\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/MultiParty/OpentokMultiparty.ts"],"names":[],"mappings":"AACA,OAAO,EAA4C,UAAU,EAA4C,UAAU,EAAC,MAAM,cAAc,CAAC;AAIzI,OAAO,EAAC,YAAY,EAAE,gBAAgB,EAAC,MAAM,aAAa,CAAC;AAK3D,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAUjC,qBAAa,iBAAkB,SAAQ,UAAU;IAC7C,WAAW,EAAE,KAAK,CAAC,UAAU,CAAC,CAAM;IACpC,OAAO,CAAC,EAAE,EAAE,CAAC,OAAO,GAAG,IAAI,CAAC;IAC5B,gBAAgB,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC;IAEhC,gDAAgD;IAChD,WAAW,CAAC,EAAE,YAAY,CAAC;IAC3B,gBAAgB,SAAK;IAErB,IAAI,eAAe,IAAI,MAAM,GAAG,SAAS,CAExC;IAED,OAAO,gBAAiB,YAAY,KAAG,IAAI,CAmCzC;IAEF,UAAU,QAAO,IAAI,CAYnB;IAEF,sBAAsB,aAAoB,gBAAgB,KAAG,QAAQ,OAAO,GAAG,KAAK,CAAC,CAYnF;IAEF,sBAAsB,aAAoB,gBAAgB,KAAG,QAAQ,OAAO,GAAG,KAAK,CAAC,CASnF;IAEF,eAAe,yBAAgC,gBAAgB,KAAG,QAAQ,IAAI,CAAC,CA2B7E;IAEF,YAAY,UAAiB,OAAO,KAAG,QAAQ,IAAI,CAAC,CAgBlD;IAEF,YAAY,UAAiB,OAAO,KAAG,QAAQ,IAAI,CAAC,CAgBlD;IAEF,YAAY,QAAa,QAAQ,IAAI,CAAC,CAiCpC;IAEF,uEAAuE;IACvE,OAAO,CAAC,aAAa;IAIrB,aAAa,aAAc,gBAAgB,KAAG,IAAI,CAiChD;IAEF,oBAAoB;gBACR,GAAG,MAAM;UACjB,IAAI,CAuCN;IAEF,sBAAsB;gBACV,GAAG,MAAM;gBACT,MAAM;UACd,IAAI,CAON;IAEF,wBAAwB;oBACR,GAAG,UAAU;UACzB,IAAI,CAaN;IAEF,0BAA0B;oBACV,GAAG,UAAU;gBACjB,MAAM;UACd,IAAI,CAYN;IAEF,kBAAkB,iCAAoB,IAAI,CAExC;CACL","file":"OpentokMultiparty.d.ts","sourcesContent":["\nimport {maxParticipantsNumber, MULTI_PARTY_EVENTS, MultiParty, participantDomId, pinnedParticipantDomId, Subscriber} from './MultiParty';\nimport {CameraTypes, SessionClientRole} from '../MediaConstants';\nimport {getMediaTracer} from '../MediaUtils/MediaTracer';\nimport get from 'lodash/get';\nimport {ICredentials, ISessionSettings} from './Contracts';\nimport filter from 'lodash/filter';\nimport cloneDeep from 'lodash/cloneDeep';\nimport {MediaCapabilitiesHelper} from './MediaCapabilitiesHelper';\n\nimport OT from '@opentok/client';\nconst trace = getMediaTracer('Multiparty');\n\n// The preferred camera position to use for the video source\nenum OpentokFacingMode {\n USER = 'user',\n ENVIRONMENT = 'environment'\n}\n\n\nexport class OpentokMultiparty extends MultiParty {\n subscribers: Array<Subscriber> = [];\n session?: OT.Session | null;\n currentPublisher?: OT.Publisher;\n \n /** @FIXME not used atm, should be refactored */\n credentials?: ICredentials;\n participantCount = 0;\n\n get ownConnectionId(): string | undefined {\n return this.session?.connection?.connectionId;\n }\n\n connect = (credentials: ICredentials): void => {\n this.credentials = credentials;\n\n try {\n this.session = OT.initSession(credentials.apiKey, credentials.sessionId);\n } catch (err) {\n trace.error(JSON.stringify(err));\n this.emit(MULTI_PARTY_EVENTS.connectFailed, {meta: {err: JSON.stringify(err)}});\n\n return;\n }\n\n this.session.on('connectionCreated', this.connectionCreatedHandler);\n this.session.on('connectionDestroyed', this.connectionDestroyedHandler);\n this.session.on('streamCreated', this.streamCreatedHandler);\n this.session.on('streamDestroyed', this.streamDestroyedHandler);\n\n this.session.connect(credentials.token, async (error?: OT.OTError) => {\n if (error) {\n this.session?.off();\n this.session = null;\n this.traceErrorCallback(error);\n this.emit(MULTI_PARTY_EVENTS.connectFailed, {meta: {err: JSON.stringify(error)}});\n\n return;\n }\n\n if (this.settings.defaultCamera === CameraTypes.NONE && this.settings.startWithAudioMuted) {\n trace.info('camera is none, no need to create publisher');\n\n return;\n }\n\n await this.createPublisher(this.settings);\n });\n };\n\n disconnect = (): void => {\n try {\n trace.info('Disconnecting from session');\n\n if (this.session) {\n this.session.disconnect();\n this.participantCount = 0;\n trace.info('Disconnection from session completed');\n }\n } catch (err) {\n trace.error(JSON.stringify(err));\n }\n };\n\n couldStartPublishVideo = async (settings: ISessionSettings): Promise<boolean | Error> => {\n try {\n const mediaCapabilitiesHelper = await MediaCapabilitiesHelper.init();\n\n return (settings.defaultCamera !== CameraTypes.NONE)\n ? mediaCapabilitiesHelper.hasCamera()\n : false;\n\n } catch (err) {\n trace.error(JSON.stringify(err));\n throw err;\n }\n };\n\n couldStartPublishAudio = async (settings: ISessionSettings): Promise<boolean | Error> => {\n try {\n const mediaCapabilitiesHelper = await MediaCapabilitiesHelper.init();\n\n return !settings.startWithAudioMuted && mediaCapabilitiesHelper.hasMicrophone();\n } catch (err) {\n trace.error(JSON.stringify(err));\n throw err;\n }\n };\n\n createPublisher = async (settingsForPublisher: ISessionSettings): Promise<void> => {\n const settings = cloneDeep(settingsForPublisher);\n const couldStartPublishVideo = await this.couldStartPublishVideo(settings);\n const couldStartPublishAudio = await this.couldStartPublishAudio(settings);\n\n if (!couldStartPublishVideo && !couldStartPublishAudio) {\n // FIXME: use emitter\n trace.error('could not create a publisher, due to lack of media capabilities or settings');\n\n return;\n }\n\n if (!couldStartPublishVideo) {\n settings.defaultCamera = CameraTypes.NONE;\n }\n\n if (!couldStartPublishAudio) {\n settings.startWithAudioMuted = true;\n }\n\n if (!this.currentPublisher) {\n this.initPublisher(settings);\n if (!this.currentPublisher) {\n throw new Error('Creating session requires initialized publisher');\n }\n this.currentPublisher && this.session?.publish(this.currentPublisher, this.traceErrorCallback);\n }\n };\n\n publishAudio = async (value: boolean): Promise<void> => {\n const settings = cloneDeep(this.settings);\n\n settings.startWithAudioMuted = !value;\n\n if (value && !await this.couldStartPublishAudio(settings)) {\n trace.error('could not publish audio');\n\n return;\n }\n\n if (!this.currentPublisher) {\n await this.createPublisher(settings);\n } else {\n this.currentPublisher.publishAudio(value);\n }\n };\n\n publishVideo = async (value: boolean): Promise<void> => {\n const settings = cloneDeep(this.settings);\n\n settings.defaultCamera = value ? this.settings.defaultCamera : CameraTypes.NONE;\n\n if (value && !await this.couldStartPublishVideo(settings)) {\n trace.error('could not publish video');\n\n return;\n }\n\n if (!this.currentPublisher) {\n await this.createPublisher(settings);\n } else {\n this.currentPublisher.publishVideo(value);\n }\n };\n\n switchCamera = async (): Promise<void> => {\n try {\n const mediaCapabilitiesHelper = await MediaCapabilitiesHelper.init();\n\n if (!mediaCapabilitiesHelper.supportSwitchCamera) {\n throw new Error('Not Support switch camera');\n }\n\n let deviceId = '';\n let cameraType = CameraTypes.NONE;\n\n switch (this.settings.defaultCamera) {\n case CameraTypes.BACK:\n cameraType = CameraTypes.FRONT;\n deviceId = mediaCapabilitiesHelper.frontCameraDeviceId;\n break;\n case CameraTypes.FRONT:\n cameraType = CameraTypes.BACK;\n deviceId = mediaCapabilitiesHelper.backCameraDeviceId;\n break;\n default:\n throw new Error(`cameraType not valid: ${this.settings.defaultCamera}`);\n }\n\n return this.currentPublisher?.setVideoSource(deviceId)\n .then(() => {\n this.settings.defaultCamera = cameraType;\n trace.info(`Success to switch camera to cameraType: ${cameraType}`);\n });\n } catch (err) {\n trace.error(JSON.stringify(err));\n throw err;\n }\n };\n\n /** @FIXME 'default' role is a temporary workaround for BE/Storybook */\n private getClientRole(connection?: OT.Connection): SessionClientRole | 'default' {\n return (connection?.data && JSON.parse(connection.data).clientRole) || 'default';\n }\n\n initPublisher = (settings: ISessionSettings): void => {\n const properties: OT.PublisherProperties = {\n insertMode: 'append',\n fitMode: 'cover',\n width: '100%',\n height: '100%',\n publishAudio: !settings.startWithAudioMuted,\n publishVideo: settings.defaultCamera !== CameraTypes.NONE,\n facingMode: settings.defaultCamera === CameraTypes.FRONT ? OpentokFacingMode.USER : OpentokFacingMode.ENVIRONMENT\n };\n\n const clientRole = this.getClientRole(this.session?.connection);\n const clientDiv = (clientRole === SessionClientRole.AGENT || clientRole === 'default')\n ? pinnedParticipantDomId\n : `${participantDomId}${this.participantCount}`;\n\n this.currentPublisher = OT.initPublisher(clientDiv, properties, this.traceErrorCallback);\n\n if (!this.currentPublisher) {\n trace.error('failed to init publisher');\n } else {\n this.currentPublisher.on('videoElementCreated', (event: any) => {\n trace.info('videoElementCreated', event);\n this.emit(MULTI_PARTY_EVENTS.videoElementCreated, event);\n });\n this.currentPublisher.on('streamDestroyed', (event: any) => {\n trace.info('Publisher stream destroyed', event);\n });\n this.currentPublisher.on('streamCreated', (event: any) => {\n this.emit(MULTI_PARTY_EVENTS.streamCreated, event);\n trace.info('publisher stream created', event, get(this.currentPublisher, 'stream.hasAudio'));\n });\n }\n };\n\n streamCreatedHandler = (event: OT.Event<'streamCreated', OT.Session> & {\n stream: OT.Stream;\n }): void => {\n trace.info('streamCreatedHandler', event.stream);\n this.emit(MULTI_PARTY_EVENTS.streamCreated, event);\n\n const {stream} = event;\n const {connection} = stream || {connection: null};\n\n if (!this.ownConnectionId || !stream || connection.connectionId === this.ownConnectionId) {\n return;\n }\n\n if (this.subscribers.length >= maxParticipantsNumber - 1) {\n trace.info(`a new subscriber cannot be added, as the number of participants reached its limit: ${maxParticipantsNumber}`);\n } else {\n // eslint-disable-next-line no-constant-condition\n const clientDiv = (this.getClientRole(connection) === SessionClientRole.AGENT)\n ? pinnedParticipantDomId\n : `${participantDomId}${this.participantCount}`;\n const properties: OT.SubscriberProperties = {\n insertMode: 'append',\n fitMode: 'cover',\n width: '100%',\n height: '100%'\n };\n\n const subscriber = event.target.subscribe(event.stream, clientDiv, properties, (err: any) => {\n this.traceErrorCallback(err);\n\n subscriber?.on('videoElementCreated', (event: any) => {\n trace.info('videoElementCreated', event);\n });\n });\n\n this.subscribers.push({\n subscriber,\n connectionId: connection.connectionId,\n clientRole: this.getClientRole(connection)\n });\n }\n };\n\n streamDestroyedHandler = (event: OT.Event<'streamDestroyed', OT.Session> & {\n stream: OT.Stream;\n reason: string;\n }): void => {\n trace.info('streamDestroyedHandler', event);\n const {stream} = event;\n const destroyedConnectionId = stream && stream.connection && stream.connection.connectionId;\n\n this.emit(MULTI_PARTY_EVENTS.streamDestroyed, {meta: {event: JSON.stringify(event)}});\n this.subscribers = filter(this.subscribers, (subscriber) => subscriber.connectionId !== destroyedConnectionId);\n };\n\n connectionCreatedHandler = (event: OT.Event<'connectionCreated', OT.Session> & {\n connection: OT.Connection;\n }): void => {\n trace.info('connectionCreatedHandler', event);\n const {connection} = event;\n\n if (!this.ownConnectionId || !connection) {\n return;\n }\n\n ++this.participantCount;\n this.emit(MULTI_PARTY_EVENTS.participantsNumberChanged, this.participantCount);\n const clientRole = this.getClientRole(connection);\n\n this.emit(MULTI_PARTY_EVENTS.connectionCreated, {meta: {clientRole, event: JSON.stringify(event)}});\n };\n\n connectionDestroyedHandler = (event: OT.Event<'connectionDestroyed', OT.Session> & {\n connection: OT.Connection;\n reason: string;\n }): void => {\n const {connection} = event;\n\n if (!this.ownConnectionId || !connection || connection.connectionId !== this.ownConnectionId) {\n return;\n }\n\n --this.participantCount;\n this.emit(MULTI_PARTY_EVENTS.participantsNumberChanged, this.participantCount);\n this.session = null;\n trace.info('connectionDestroyedHandler', event);\n this.emit(MULTI_PARTY_EVENTS.connectionDestroyed, {meta: {event: JSON.stringify(event)}});\n };\n\n traceErrorCallback = (error?: Error): void => {\n trace.error(JSON.stringify(error));\n };\n}\n"]}
|