@techteamer/janus-api
Version:
Janus WebRTC-Gateway API
481 lines (412 loc) • 12.7 kB
JavaScript
import WebSocket from 'isomorphic-ws'
import JanusPlugin from './JanusPlugin.js'
import { v4 as uuid } from 'uuid'
const ignoredErrorCodes = [
458, // JANUS_ERROR_SESSION_NOT_FOUND
459 // JANUS_ERROR_HANDLE_NOT_FOUND
]
class Janus {
constructor (config, logger) {
this.ws = undefined
this.isConnected = false
this.sessionId = undefined
this.logger = logger
this.transactions = {}
this.pluginHandles = {}
this.config = config
this.protocol = 'janus-protocol'
this.sendCreate = true
}
connect () {
if (this.isConnected) {
return Promise.resolve(this)
}
return new Promise((resolve, reject) => {
this.ws = new WebSocket(this.config.url, this.protocol, this.config.options)
this.ws.addEventListener('error', (err) => {
this.logger.error('Error connecting to the Janus WebSockets server...', err)
this.isConnected = false
reject(err)
})
this.ws.addEventListener('close', () => {
this.cleanup()
})
this.ws.addEventListener('open', () => {
if (!this.sendCreate) {
this.isConnected = true
this.keepAlive(true)
return resolve(this)
}
const transaction = uuid()
const request = { janus: 'create', transaction }
this.transactions[transaction] = {
resolve: (json) => {
if (json.janus !== 'success') {
this.logger.error('Cannot connect to Janus', json)
reject(json)
return
}
this.sessionId = json.data.id
this.isConnected = true
this.keepAlive(true)
this.logger.debug('Janus connected, sessionId: ', this.sessionId)
resolve(this)
},
reject,
replyType: 'success'
}
this.websocketSend(JSON.stringify(request))
})
this.ws.addEventListener('message', (event) => {
this.onMessage(event)
})
this.ws.addEventListener('close', () => {
this.onClose()
})
})
}
/**
*
* @param {JanusPlugin} plugin
* @return {Promise}
* */
addPlugin (plugin) {
if (!(plugin instanceof JanusPlugin)) {
return Promise.reject(new Error('plugin is not a JanusPlugin'))
}
const request = plugin.getAttachPayload()
return this.transaction('attach', request, 'success').then((json) => {
if (json.janus !== 'success') {
this.logger.error('Cannot add plugin', json)
plugin.error(json)
throw new Error(json)
}
this.pluginHandles[json.data.id] = plugin
return plugin.success(this, json.data.id)
})
}
transaction (type, payload, replyType, timeoutMs) {
if (!replyType) {
replyType = 'ack'
}
const transactionId = uuid()
return new Promise((resolve, reject) => {
if (timeoutMs) {
setTimeout(() => {
reject(new Error('Transaction timed out after ' + timeoutMs + ' ms'))
}, timeoutMs)
}
if (!this.isConnected) {
reject(new Error('Janus is not connected'))
return
}
const request = Object.assign({}, payload, {
janus: type,
session_id: (payload && parseInt(payload.session_id, 10)) || this.sessionId,
transaction: transactionId
})
this.transactions[request.transaction] = { resolve, reject, replyType, request }
this.websocketSend(JSON.stringify(request))
})
}
send (type, payload) {
return new Promise((resolve, reject) => {
if (!this.isConnected) {
reject(new Error('Janus is not connected'))
return
}
const request = Object.assign({}, payload, {
janus: type,
session_id: this.sessionId,
transaction: uuid()
})
this.logger.debug('Janus sending', request)
this.websocketSend(JSON.stringify(request), {}, (err) => {
if (err) {
reject(err)
} else {
resolve()
}
})
})
}
/**
* Send a data message.
*
* @param {*} data The message to send
* @param {Object} options Options object
* @param {Function} cb Callback which is executed when data is written out
* @public
*/
websocketSend (data, options, cb) {
try {
this.ws.send(data, options, cb)
} catch (err) {
if (cb) {
cb(err)
}
this.logger.error('Websocket send method exception', err)
}
}
destroy () {
if (!this.isConnected) {
return Promise.resolve()
}
return this.transaction('destroy', {}, 'success', 5000).then(() => {
this.cleanup()
}).catch(() => {
this.cleanup()
})
}
destroyPlugin (plugin) {
return new Promise((resolve, reject) => {
if (!(plugin instanceof JanusPlugin)) {
reject(new Error('plugin is not a JanusPlugin'))
return
}
if (!this.pluginHandles[plugin.janusHandleId]) {
reject(new Error('unknown plugin'))
return
}
this.transaction('detach', { plugin: plugin.pluginName, handle_id: plugin.janusHandleId }, 'success', 5000).then(() => {
delete this.pluginHandles[plugin.janusHandleId]
plugin.detach()
resolve()
}).catch((err) => {
delete this.pluginHandles[plugin.janusHandleId]
plugin.detach()
reject(err)
})
})
}
onMessage (messageEvent) {
let json
try {
json = JSON.parse(messageEvent.data)
} catch (err) {
this.logger.error('cannot parse message', messageEvent.data)
return
}
// this.logger.debug('JANUS GOT', json)
if (json.janus === 'timeout' && json.session_id !== this.sessionId) {
this.logger.debug('GOT timeout from another websocket') // seems like a bug in janus timeout handler :)
return
}
if (json.janus === 'keepalive') { // Do nothing
return
}
if (json.janus === 'ack') { // Just an ack, we can probably ignore
const transaction = this.getTransaction(json)
if (transaction && transaction.resolve) {
transaction.resolve(json)
}
return
}
if (json.janus === 'success') { // Success!
const transaction = this.getTransaction(json)
if (!transaction) {
return
}
const pluginData = json.plugindata
if (pluginData === undefined || pluginData === null) {
transaction.resolve(json)
return
}
const sender = json.sender
if (!sender) {
transaction.resolve(json)
this.logger.error('Missing sender for plugindata', json)
return
}
const pluginHandle = this.pluginHandles[sender]
if (!pluginHandle) {
this.logger.error('This handle is not attached to this session', json)
return
}
transaction.resolve({ data: pluginData.data, json })
return
}
if (json.janus === 'webrtcup') { // The PeerConnection with the gateway is up! Notify this
const sender = json.sender
if (!sender) {
this.logger.warn('Missing sender...')
return
}
const pluginHandle = this.pluginHandles[sender]
if (!pluginHandle) {
this.logger.error('This handle is not attached to this session', sender)
return
}
pluginHandle.webrtcState(true)
return
}
if (json.janus === 'hangup') { // A plugin asked the core to hangup a PeerConnection on one of our handles
const sender = json.sender
if (!sender) {
this.logger.warn('Missing sender...')
return
}
const pluginHandle = this.pluginHandles[sender]
if (!pluginHandle) {
this.logger.error('This handle is not attached to this session', sender)
return
}
pluginHandle.webrtcState(false, json.reason)
pluginHandle.hangup()
return
}
if (json.janus === 'detached') { // A plugin asked the core to detach one of our handles
const sender = json.sender
if (!sender) {
this.logger.warn('Missing sender...')
return
}
return
}
if (json.janus === 'media') { // Media started/stopped flowing
const sender = json.sender
if (!sender) {
this.logger.warn('Missing sender...')
return
}
const pluginHandle = this.pluginHandles[sender]
if (!pluginHandle) {
this.logger.error('This handle is not attached to this session', sender)
return
}
pluginHandle.mediaState(json.type, json.receiving)
return
}
if (json.janus === 'slowlink') { // Trouble uplink or downlink
this.logger.debug('Got a slowlink event on session ' + this.sessionId)
this.logger.debug(json)
const sender = json.sender
if (!sender) {
this.logger.warn('Missing sender...')
return
}
const pluginHandle = this.pluginHandles[sender]
if (!pluginHandle) {
this.logger.error('This handle is not attached to this session', sender)
return
}
pluginHandle.slowLink(json.uplink, json.nacks)
return
}
if (json.janus === 'error') { // Oops, something wrong happened
if (json.error && json.error.code && !ignoredErrorCodes.includes(json.error.code)) {
this.logger.error('Janus error response' + json)
}
const transaction = this.getTransaction(json, true)
if (transaction && transaction.reject) {
if (transaction.request) {
this.logger.debug('Janus Error: rejecting transaction', transaction.request, json)
}
transaction.reject(json)
}
return
}
if (json.janus === 'event') {
const sender = json.sender
if (!sender) {
this.logger.warn('Missing sender...')
return
}
const pluginData = json.plugindata
if (pluginData === undefined || pluginData === null) {
this.logger.error('Missing plugindata...')
return
}
const pluginHandle = this.pluginHandles[sender]
if (!pluginHandle) {
this.logger.error('This handle is not attached to this session', sender)
return
}
const data = pluginData.data
const transaction = this.getTransaction(json)
if (transaction) {
if (data.error_code) {
transaction.reject({ data, json })
} else {
transaction.resolve({ data, json })
}
return
}
pluginHandle.onmessage(data, json)
return
}
this.logger.warn('Unknown message/event ' + json.janus + ' on session ' + this.sessionId)
this.logger.debug(json)
}
onClose () {
if (!this.isConnected) {
return
}
this.isConnected = false
this.logger.error('Lost connection to the gateway (is it down?)')
}
keepAlive (isScheduled) {
if (!this.ws || !this.isConnected || !this.sessionId) {
return
}
if (isScheduled) {
setTimeout(() => {
this.keepAlive()
}, this.config.keepAliveIntervalMs)
} else {
// logger.debug('Sending Janus keepalive')
this.transaction('keepalive').then(() => {
setTimeout(() => {
this.keepAlive()
}, this.config.keepAliveIntervalMs)
}).catch((err) => {
this.logger.warn('Janus keepalive error', err)
})
}
}
getTransaction (json, ignoreReplyType = false) {
const type = json.janus
const transactionId = json.transaction
if (transactionId &&
Object.prototype.hasOwnProperty.call(this.transactions, transactionId) &&
(ignoreReplyType || this.transactions[transactionId].replyType === type)) {
const ret = this.transactions[transactionId]
delete this.transactions[transactionId]
return ret
}
}
cleanup () {
this._cleanupPlugins()
this._cleanupWebSocket()
this._cleanupTransactions()
}
_cleanupWebSocket () {
if (this.ws) {
if (this.ws.removeAllListeners) {
this.ws.removeAllListeners()
}
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.close()
}
}
this.ws = undefined
this.isConnected = false
}
_cleanupPlugins () {
Object.keys(this.pluginHandles).forEach((pluginId) => {
const plugin = this.pluginHandles[pluginId]
delete this.pluginHandles[pluginId]
plugin.detach()
})
}
_cleanupTransactions () {
Object.keys(this.transactions).forEach((transactionId) => {
const transaction = this.transactions[transactionId]
if (transaction.reject) {
transaction.reject()
}
})
this.transactions = {}
}
}
export default Janus