import onChange from 'on-change' import { Inviter, Registerer, SessionState, UserAgent } from 'sip.js' import { UserAgentDelegate } from 'sip.js/lib/api/user-agent-delegate' import { Invitation } from 'sip.js/lib/api/invitation' import { SessionDescriptionHandler } from 'sip.js/lib/api/session-description-handler' import { UserAgentOptions } from 'sip.js/lib/api/user-agent-options' import { userAgentDefault } from './config' import { userAgentStatus } from './const' type UserAgentDelegateKey = keyof UserAgentDelegate type UserAgentStatusKey = keyof typeof userAgentStatus export class UserAgentFactory { static #userAgentManager: InstanceType static getUserAgentManager (options: Partial = {}) { if (!this.#userAgentManager) { this.#userAgentManager = new UserAgentManager(options) } return this.#userAgentManager } static hasUserAgentManager () { return !!this.#userAgentManager } static newUserAgentManager (options: Partial = {}) { return new UserAgentManager(options) } } export class UserAgentManager { #userAgent: InstanceType | undefined = undefined #sessionDescriptionHandler: SessionDescriptionHandler | undefined = undefined #peerConnection: RTCPeerConnection | undefined = undefined #receivers: RTCRtpReceiver[] | [] = [] #senders: RTCRtpSender[] | [] = [] #userAgentOption: Partial = {} #currentInvitation: Invitation | undefined = undefined #currentInviter: InstanceType | undefined = undefined #stream: MediaStream | undefined = new MediaStream() #userAgentStatus: typeof userAgentStatus = JSON.parse(JSON.stringify(userAgentStatus)) #empty = () => { } constructor (userAgentOption = {}) { if (typeof userAgentOption !== 'object') { throw new Error('userAgentOption must be plain object') } this.#userAgentOption = this.getUserAgentOption(userAgentOption) } initStatus (config: { refresh: Function }) { // @ts-ignore this.#userAgentStatus = onChange(JSON.parse(JSON.stringify(userAgentStatus)), typeof config.refresh === 'function' ? config.refresh : this.#empty) } getUserAgentOption (userAgentOption: Partial) { const initUserAgentOption: Partial = { delegate: { onInvite: (invitation: Invitation) => { this.clearCurrentInvitation() this.#currentInvitation = invitation this.#userAgentStatus.incomingStatus = true invitation.stateChange.addListener(this.receivedCall) } } } if (userAgentOption.delegate) { Object.keys(userAgentOption.delegate).forEach((userAgentDelegateKey) => { const key = userAgentDelegateKey as UserAgentDelegateKey if (userAgentOption.delegate![key] && typeof userAgentOption.delegate![key] === 'function') { const tmp = userAgentOption.delegate![key] as Function // @ts-ignore userAgentOption.delegate[key] = async (invitation: Invitation) => { // @ts-ignore initUserAgentOption.delegate![key]?.(invitation) await tmp!(invitation) } } }) } else { userAgentOption.delegate = initUserAgentOption.delegate } return { ...userAgentDefault, ...userAgentOption } as UserAgentOptions } getUserAgentStatue () { return this.#userAgentStatus } getUserAgent () { if (!this.#userAgent) { this.#userAgent = new UserAgent(this.#userAgentOption) } return this.#userAgent } getSessionDescriptionHandler () { return this.#sessionDescriptionHandler } getPeerConnection () { return this.#peerConnection } getSenders () { return this.#senders } getReceivers () { return this.#receivers } getStream () { if (!this.#stream) { this.#stream = new MediaStream() } this.#receivers.forEach((receiver) => { if (receiver.track) { this.#stream!.addTrack(receiver.track) } }) return this.#stream } getCurrentInviter () { if (!this.#currentInviter) { throw new Error('No currentInviter, please call createCurrentInviter') } return this.#currentInviter } getCurrentInvitation () { return this.#currentInvitation } clearCurrentInvitation () { if (this.#currentInvitation) { this.#currentInvitation.dispose() this.#currentInvitation = undefined } } clearCurrentInviter () { if (this.#currentInviter) { this.#currentInviter.dispose() this.#currentInviter = undefined } } clearUserAgent () { if (this.#userAgent) { this.#userAgent.stop() this.#userAgent = undefined } } clearStream () { if (this.#stream) { this.#stream.getTracks().forEach(track => { track.stop() }) this.#stream = undefined } } clearUserAgentStatus () { Object.keys(userAgentStatus).forEach(userAgentStatusKey => { const key = userAgentStatusKey as UserAgentStatusKey this.#userAgentStatus[key] = userAgentStatus[key] }) } dispose () { this.clearUserAgentStatus() this.clearStream() this.clearSenders() this.clearReceivers() this.clearPeerConnection() this.clearSessionDescriptionHandler() this.clearUserAgent() } clearSessionDescriptionHandler () { if (this.#sessionDescriptionHandler) { this.#sessionDescriptionHandler.close() this.#sessionDescriptionHandler = undefined } } clearPeerConnection () { if (this.#peerConnection) { this.#peerConnection.close() this.#peerConnection = undefined } } clearSenders () { this.#senders = [] } clearReceivers () { this.#receivers = [] } setupSession () { const invitation = this.getCurrentInvitation() const { sessionDescriptionHandler } = invitation! this.clearPeerConnection() this.clearSessionDescriptionHandler() this.clearSenders() this.clearReceivers() this.#sessionDescriptionHandler = sessionDescriptionHandler const sessionDescriptionHandlerType = sessionDescriptionHandler as Record this.#peerConnection = sessionDescriptionHandlerType.peerConnection as RTCPeerConnection this.#senders = this.#peerConnection.getSenders() this.#receivers = this.#peerConnection.getReceivers() // this.muteLocalAudio() } /** * 接听过程 * @param state */ receivedCall = (state: SessionState) => { switch (state) { case SessionState.Initial: break case SessionState.Establishing: break case SessionState.Established: this.setupSession() break case SessionState.Terminating: // fall through case SessionState.Terminated: break default: throw new Error('Unknown session state.') } } async createCurrentInviter (uri: string) { const target = UserAgent.makeURI(uri) if (!target) { throw new Error('Failed to create target URI.') } this.#currentInviter = new Inviter(this.getUserAgent(), target, { sessionDescriptionHandlerOptions: { constraints: { audio: true, video: false } } }) return this.#currentInviter } async sendCurrentInviter () { await this.getCurrentInviter().invite() } /** * 忽略 */ async ignoreInvite () { this.#userAgentStatus.incomingStatus = false await this.getCurrentInvitation()!.reject() } /** * 接听 */ async acceptInvite () { this.#userAgentStatus.incomingStatus = false await this.getCurrentInvitation()!.accept({ sessionDescriptionHandlerOptions: { constraints: { audio: true, video: false } } }) this.#userAgentStatus.answerStatus = true } /** * 挂断 */ async hangUpInvite () { switch (this.#currentInvitation!.state) { case SessionState.Initial: case SessionState.Establishing: // if (this.#currentInvitation instanceof Inviter) { // // An unestablished outgoing session // this.#currentInvitation!.cancel() // } else { // An unestablished incoming session await this.#currentInvitation!.reject() // } break case SessionState.Established: // An established session await this.#currentInvitation!.bye() break case SessionState.Terminating: case SessionState.Terminated: // Cannot terminate a session that is already terminated break } this.#userAgentStatus.answerStatus = false } /** * 不接听远端声音 */ muteRemoteAudio () { this.#peerConnection!.getReceivers().forEach(item => { item.track.enabled = false }) } /** * 接听远端声音 */ unMuteRemoteAudio () { this.#peerConnection!.getReceivers().forEach(item => { item.track.enabled = true }) } /** * 不传输本地声音到远端 */ muteLocalAudio () { this.#peerConnection!.getSenders().forEach(item => { item.track!.enabled = false }) } /** * 传输本地声音到远端 */ unMuteLocalAudio () { this.#peerConnection!.getSenders().forEach(item => { item.track!.enabled = true }) } /** * 实例管理 */ extendDelegate (delegate: UserAgentDelegate) { Object.keys(delegate).forEach(userAgentDelegateKey => { const key = userAgentDelegateKey as UserAgentDelegateKey if (typeof delegate?.[key] === 'function') { const tmp = delegate[key] as Function // @ts-ignore delegate[key] = async (invitation: Invitation) => { // @ts-ignore this.#userAgentOption.delegate![key]?.(invitation) await tmp(invitation) } } }) return delegate } bindUserAgent (delegate: UserAgentDelegate) { const userAgent = this.getUserAgent() userAgent.delegate = this.extendDelegate(delegate) return userAgent } async connectUserAgent (config: { refresh: Function }) { this.initStatus(config) const userAgent = this.getUserAgent() await userAgent.start() if (!userAgent.isConnected()) { // 二次重连 await userAgent.reconnect() } if (!userAgent.isConnected()) { throw new Error('链接失败,请稍后再试') } this.#userAgentStatus.connectStatus = true return userAgent } async registerUserAgent () { const userAgent = this.getUserAgent() const registerer = new Registerer(userAgent) await registerer.register() this.#userAgentStatus.registerStatus = true return userAgent } async prepareUserAgent (config: { refresh: Function }, delegate: UserAgentDelegate) { await this.connectUserAgent(config) this.bindUserAgent(delegate) await this.registerUserAgent() } }