vroom-web-sdk-beta
Version:
VROOM SDK (beta) by True Virtual World
582 lines (477 loc) • 15.3 kB
text/typescript
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)
}
}