UNPKG

vroom-web-sdk-beta

Version:

VROOM SDK (beta) by True Virtual World

582 lines (477 loc) 15.3 kB
import { sdkProtocol, WS_EVENT, VROOM_COMMAND_STATUS, VROOM_SDK_EVENT, VROOM_COMMAND, BITRATE } from './constants' import SdkBase from './sdk/sdkBase' import { get, findIndex } from 'lodash' import Configs from './config' import { JanusMessage } from './types/vroomSDK.base' import wsEventFn from './wsEvents/index.wsEventFn' import VroomSession from './session/vroom.session' import errorMessage from './constants/errorMessage' import transactionFn from './transactions/index.transactionFn' import VroomVideoPlugin, { VroomVideoPluginDelegate } from './plugins/vroom-video.plugin' import VroomParticipant from './types/publisher' import isMobileHelper from './helpers/isMobile.helper' /** * @class VroomSDK * @extends SdkBase */ export class VroomSDK extends SdkBase implements VroomVideoPluginDelegate { private pub: VroomVideoPlugin | any private sub: VroomVideoPlugin | any public subMediaList: any[] = [] public attendees: any[] = [] public roomInfo: any private devices: MediaDeviceInfo[] = [] private currentCamera?: MediaDeviceInfo // handle delegate onJoinRoomComplete: any onJoinRoomFailure: any onStreamUpdated: any onParticipantJoined: any onParticipantLeave: any onParticipantMuteAudio: any onParticipantMuteVideo: any public async validaToken() { return fetch('https://api.spacexdata.com/v3/capsules?limit=1&offset=1').then(res => res.json()) } public async getWebSocketFromServer(newVroomUrl: string) { const room = this.getRoomIdFromUrl(newVroomUrl) return await fetch( `${Configs.loadBalanceUrl}/${room}`, { headers: { Authorization: Configs.basicAuthLoadBalancer, } } ) .then(res => res.json()) .then(res => { return new WebSocket(res.wss, sdkProtocol) }) .catch(console.error) } public emitParent() { this.emit(VROOM_SDK_EVENT.MONITOR, this) } public emitCreated() { this.emit(VROOM_SDK_EVENT.CREATED, this) } public handleToggleAudio(muted: boolean) { this.toggleAudio(muted) .then(() => { this.emitParent() }) .catch(console.error) } public handleToggleVideo(muted: boolean) { this.toggleVideo(muted) .then(() => { this.emitParent() }) .catch(console.error) } // TODO : can remove if (sdk) public onMuteCli(sdk: VroomSDK, muted: boolean, type: string) { if (sdk) { switch (type) { case 'audio': sdk.handleToggleAudio(muted) break case 'video': sdk.handleToggleVideo(muted) break } } } public async onSwitchCamera(sdk: VroomSDK, device: MediaDeviceInfo, isFrontCam = true) { await sdk.switchCamera(device, isFrontCam) } public async onHangupCall(sdk: VroomSDK) { await sdk.hangupCall() } public async initLocalStream(audio: boolean, video: boolean) { let videoContain = { audio: audio, video: video, } // TODO : create videoContain Type if (isMobileHelper()) { videoContain = { audio: audio, // @ts-ignore video: { facingMode: 'user' }, } } return navigator .mediaDevices .getUserMedia(videoContain) .then(async (stream: MediaStream) => { navigator.mediaDevices.enumerateDevices() .then((devices: MediaDeviceInfo[]) => { devices .filter((d) => d.kind === 'videoinput') .forEach((device: MediaDeviceInfo) => { this.devices[this.devices.length] = device stream .getTracks() .forEach((track: MediaStreamTrack) => { if (track.getSettings().deviceId === device.deviceId) { this.currentCamera = device } }) }) }) return stream }) } public async changeCamera(device: MediaDeviceInfo) { if (device.kind !== 'videoinput' || !device || device.deviceId === this.currentCamera?.deviceId) return const stream = await navigator.mediaDevices.getUserMedia({ video: { deviceId: device.deviceId }, audio: true }) const currentStream: MediaStream = this.pub.stream currentStream.getVideoTracks().forEach((track: MediaStreamTrack) => track.stop()) const [videoTrack] = stream.getVideoTracks() const pc: RTCPeerConnection = this.pub.peerConnection const sender: RTCRtpSender = pc.getSenders().find((s: any) => s.track.kind === videoTrack.kind)! if (sender) { sender.track?.stop() await sender.replaceTrack(videoTrack) currentStream.addTrack(videoTrack) this.emit(VROOM_SDK_EVENT.LOCAL_STREAM_UPDATE, stream) await this.pub.createOffer(BITRATE, true, true) this.pub.setStream(stream) } } public async switchCamera(inputDevice: MediaDeviceInfo | undefined, isFrontCam = true) { let device = inputDevice if (!device) return const currentStream: MediaStream = this.pub.stream currentStream.getVideoTracks().forEach((track: MediaStreamTrack) => track.stop()) let videoContain = { video: { deviceId: device.deviceId }, } if (isMobileHelper()) { videoContain = { // @ts-ignore video: { facingMode: isFrontCam ? 'user' : 'environment' }, } } navigator.mediaDevices .getUserMedia(videoContain) .then(async (stream) => { const [videoTrack] = stream.getVideoTracks() const pc: RTCPeerConnection = this.pub.peerConnection const sender: RTCRtpSender = pc.getSenders().find((s: any) => s.track.kind === videoTrack.kind)! if (sender) { sender.track?.stop() await sender.replaceTrack(videoTrack) currentStream.addTrack(videoTrack) this.currentCamera = device this.emit(VROOM_SDK_EVENT.LOCAL_STREAM_UPDATE, stream) await this.pub.createOffer(BITRATE, true, true) this.pub.setStream(stream) } }) .catch(console.error) } public getMediaCameraDevices() { return this.devices.filter((dv: MediaDeviceInfo) => dv.kind === 'videoinput') } public getMediaMicrophoneDevice() { return this.devices.filter((dv: MediaDeviceInfo) => dv.kind === 'audioinput') } public getCurrentDeviceCamera() { return this.currentCamera } public getDeviceList() { return navigator.mediaDevices.enumerateDevices() } public async hangupCall() { try { await this.sub.detachPlugin(false) await this.pub.detachPlugin(true) this.sub.close() this.pub.close() // clear delegate this.onJoinRoomComplete = null this.onJoinRoomFailure = null this.onParticipantJoined = null this.onStreamUpdated = null this.onParticipantLeave = null this.onParticipantMuteAudio = null this.onParticipantMuteVideo = null // clear state this.subMediaList = [] this.attendees = [] this.roomInfo = null this.devices = [] this.currentCamera = undefined this.sub = null this.pub = null this.emit(VROOM_SDK_EVENT.AFTER_HANGUP, true) } catch(e) { console.error('error hangup call ', e) this.emit(VROOM_SDK_EVENT.AFTER_HANGUP, false) } } constructor(config: any) { super(config) this.onJoinRoomComplete = this._onJoinRoomComplete this.onJoinRoomFailure = this._onJoinRoomFailure this.onParticipantJoined = this._onParticipantJoined this.onStreamUpdated = this._onStreamUpdated this.onParticipantLeave = this._onParticipantLeave this.onParticipantMuteAudio = this._onParticipantMuteAudio this.onParticipantMuteVideo = this._onParticipantMuteVideo this.getWebSocketFromServer(config.endpoint) .then((wsConn: WebSocket | void) => { this.wsConn = wsConn return wsConn }) .then(() => { this.wsConn.addEventListener(WS_EVENT.OPEN, (event: Event) => { const transaction = this.randomString(12) const wsEvent: WebSocket = event.target as WebSocket const roomId = this.getRoomIdFromUrl(config.endpoint) if (!roomId) { throw new Error('Room ID not found.') } transactionFn.sessionCreateRequest( JSON.stringify({janus: VROOM_COMMAND.CREATE, transaction: transaction, room_code: `${roomId}`}), wsEvent, transaction, this.mapTransactions ) }) this.wsConn.addEventListener(WS_EVENT.MESSAGE, async (event: Event) => { const eventData: JanusMessage = JSON.parse(get(event, 'data', '{}')) const reportTransaction = this.mapTransactions.get(eventData.transaction) if ( eventData.janus === VROOM_COMMAND_STATUS.SUCCESS && reportTransaction?.type === 'send-create' ) { const wsEvent: WebSocket = event.target as WebSocket const sessionId = eventData.data.id const session = new VroomSession(wsEvent, sessionId) // start heartbeat session.setKeepAliveTimeout() this.pub = new VroomVideoPlugin(session, sessionId) this.sub = new VroomVideoPlugin(session, sessionId) this.canJoin = true this.pub.delegate = this this.sub.delegate = this this.sub.peerConnection.ontrack = (trackEvent: RTCTrackEvent) => { const { transceiver, track } = trackEvent if (transceiver?.mid) { const result = this.streams.find((v) => v?.mid === transceiver?.mid) const { feed_id, type } = result if (!!feed_id && !!type) { const obj = { id: feed_id, mediaStream: { [type]: new MediaStream([track]) }, type } this.emit(VROOM_SDK_EVENT.UPDATE_TRACK, obj) } } } this.mapTransactions.delete(eventData.transaction) this.emitParent() this.emitCreated() } }) this.wsConn.addEventListener(WS_EVENT.ERROR, wsEventFn.handleErrorWs) }) .finally(() => { this.emitParent() }) .catch(() => { throw new Error(errorMessage.connectionError) }) } /** * Join after connect and create session success * * @param stream * @param audio * @param video */ public async join( stream: MediaStream, audio: boolean, video: boolean, ): Promise<void> { await this.validaToken().then(() => this.joinStart( this.pub, this.sub, stream, audio, video, this.canJoin, this.config )) .then(() => { this.emitParent() }) .catch(() => { this.joined = false }) this.joined = true } /** * @public isAudioMuted */ public isAudioMuted() { if (!this.pub) return return this.pub.isAudioMuted() } /** * @public isVideoMuted */ public isVideoMuted() { if (!this.pub) return return this.pub.isVideoMuted() } /** * @public toggleAudio * @param mute */ public async toggleAudio(mute: boolean): Promise<void> { if (!this.pub) return await this.pub.sendMuteAudio(mute) if (mute) { this.muteAudio() } else { this.unmuteAudio() } } /** * @public toggleVideo * @param mute */ public async toggleVideo(mute: boolean): Promise<void> { if (!this.pub) return await this.pub.sendMuteVideo(mute) if (mute) { this.muteVideo() } else { this.unmuteVideo() } } /** * @public */ public async leaveRoom(): Promise<void> { if (!this.pub || !this.sub) return this.sub.detachPlugin(false) // don't disconnect websocket this.pub.detachPlugin(true) // do disconnect websocket this.roomId = 0 } /** * @private */ private muteAudio() { if (!this.pub) return this.pub.muteAudio() } /** * @private */ private unmuteAudio() { if (!this.pub) return this.pub.unmuteAudio() } /** * @private */ private muteVideo() { if (!this.pub) return this.pub.muteVideo() } /** * @private */ private unmuteVideo() { if (!this.pub) return this.pub.unmuteVideo() } /** * TODO: check duplicate log with add "streams" props * @param sub * @param streams */ public mapSubscriberStream(sub: any[], streams: any) { if (sub.length > 0) { const obj = sub.find(i => i.feed_id === streams.id && i.type === streams.type) const exist = this.subMediaList.find(i => i.id === streams.id && i.type === streams.type) if (!exist) { this.subMediaList.push(Object.assign(streams, { id: +streams.id, displayName: obj.feed_display })) this.emit(VROOM_SDK_EVENT.UPDATE_SUB_MEDIA_LIST, this.subMediaList) } } } public removeSubscriberStream(id: number) { this.subMediaList.splice(findIndex(this.subMediaList, (i) => (i.type === 'audio' && i.id === id)), 1) this.subMediaList.splice(findIndex(this.subMediaList, (i) => (i.type === 'video' && i.id === id)), 1) this.emit(VROOM_SDK_EVENT.UPDATE_SUB_MEDIA_LIST, this.subMediaList) } getPrivateId() { return get(this.roomInfo, 'private_id', null) } ////////////////////////////////// for delegate ////////////////////////////////////////// private _onJoinRoomComplete(roomInfo: any) { this.roomInfo = roomInfo this.pub.userId = roomInfo.id this.sub.roomId = roomInfo.room this.sub.privateId = roomInfo.private_id this.attendees = roomInfo.attendees || [] this.emit(VROOM_SDK_EVENT.UPDATE_ATTENDEE, roomInfo.attendees) roomInfo.publishers?.forEach((p: any) => { this.receivePublisher(this.sub, [p], this.getPrivateId()) .then(() => { this.emit(VROOM_SDK_EVENT.INIT_SUB, p) }) .catch(console.error) }) } private _onJoinRoomFailure(error: Error) { this.emit(VROOM_SDK_EVENT.JOIN_FAILURE, error) } private _onStreamUpdated(streams: any[]) { this.streams = streams this.emit(VROOM_SDK_EVENT.UPDATE_SUB, this.sub) } private _onParticipantJoined(participant: VroomParticipant) { this.emit(VROOM_SDK_EVENT.JOIN, participant) if (get(participant, 'streams', false)) { this.receivePublisher(this.sub, [participant], this.getPrivateId()) .then(() => { this.emit(VROOM_SDK_EVENT.RECEIVED_PUBLISHER, participant) }) } } private _onParticipantLeave(participant: VroomParticipant) { this.attendees = this.attendees.filter((i) => i.id !== participant.id) this.emit(VROOM_SDK_EVENT.LEAVE, participant) this.emit(VROOM_SDK_EVENT.UPDATE_ATTENDEE, this.attendees) } private _onParticipantMuteAudio(data: any) { this.emit(VROOM_SDK_EVENT.MUTE_AUDIO, data) } private _onParticipantMuteVideo(data: any) { this.emit(VROOM_SDK_EVENT.MUTE_VIDEO, data) } }