librecast-live
Version:
Live Streaming Video Platform with IPv6 Multicast
523 lines (476 loc) • 15.3 kB
JavaScript
/*
* librecast.js - librecast helper functions
*
* this file is part of LIBRECAST
*
* Copyright (c) 2017-2022 Brett Sheffield <brett@librecast.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program (see the file COPYING in the distribution).
* If not, see <http://www.gnu.org/licenses/>.
*/
'use strict'
const LIBRECAST = (function () {
// eslint-disable-next-line no-unused-vars
const Channel = class {
constructor (lctx, channelName) {
console.log('Channel constructor')
if (lctx === undefined) throw new Error('Librecast.Context required')
this.lctx = lctx
this.id = undefined
this.id2 = undefined
this.name = channelName
this.oncreate = new Promise((resolve, reject) => {
const msg = new lc.Message(channelName)
msg.opcode = lc.OP_CHANNEL_NEW
if (msg.len === 0) { reject(LibrecastException(lc.ERR_MISSING_ARG)) };
msg.token = this.lctx.callback(resolve, reject)
this.lctx.send(msg)
})
.then((msg) => {
this.id = msg.id
})
};
op (opcode, data, timeout) {
return new Promise((resolve, reject) => {
if (this.lctx.websocket.readyState === lc.WS_OPEN) {
const msg = new lc.Message(data)
msg.opcode = opcode
msg.id = this.id
msg.id2 = this.id2
msg.token = this.lctx.callback(resolve, reject, timeout)
console.log('opcode = ' + opcode + ', token = ' + msg.token)
this.lctx.send(msg)
} else {
reject(LibrecastException(lc.ERR_WEBSOCKET_NOTREADY))
}
})
}
bind (sock) {
console.log('binding channel ' + this.name + '(' + this.id + ') to socket ' + sock.id)
this.id2 = sock.id
return this.op(lc.OP_CHANNEL_BIND)
}
join () {
console.log('joining channel "' + this.name + '"')
return this.op(lc.OP_CHANNEL_JOIN)
}
part () {
console.log('parting channel "' + this.name + '"')
return this.op(lc.OP_CHANNEL_PART)
}
send (data) {
return this.op(lc.OP_CHANNEL_SEND, data, lc.NO_TIMEOUT)
}
}
const UINT32_MAX = 4294967295
// eslint-disable-next-line no-unused-vars
const Context = class {
constructor () {
console.log('Librecast context constructor')
this.url = (location.protocol === 'https:') ? 'wss://' : 'ws://'
this.url += document.location.host + '/'
this.callstack = []
this.onconnect = new Promise(resolve => { this.resolveconnect = resolve })
this.connect()
};
connect () {
console.log('Librecast.connect()')
if (window.WebSocket) {
console.log('websockets supported')
} else {
console.log('websockets unsupported')
throw new LibrecastException(lc.ERR_WEBSOCKET_UNSUPPORTED)
}
this.websocket = new WebSocket(this.url, 'librecast')
this.websocket.binaryType = 'arraybuffer'
this.websocket.onclose = (e) => { this.wsClose(e) }
this.websocket.onerror = (e) => { this.wsError(e) }
this.websocket.onmessage = (e) => { this.wsMessage(e) }
this.websocket.onopen = (e) => { this.wsOpen(e) }
};
close () {
console.log('Librecast close()')
this.websocket.close()
};
get token () {
return Math.floor(Math.random() * UINT32_MAX)
};
callback (resolve, reject, timeout, repeat) {
const token = this.token
const cb = {}
cb.resolve = resolve
cb.reject = reject
cb.created = Date.now()
cb.repeat = repeat
this.callstack[token] = cb
console.log('callback created with token = ' + token)
if (timeout !== lc.NO_TIMEOUT) {
if (timeout === undefined) { timeout = lc.DEFAULT_TIMEOUT };
console.log('setting callback timer ' + token)
cb.timeout = setTimeout(() => {
reject('callback (' + token + ') timeout')
delete this.callstack[token]
}, timeout)
}
return token
};
cancelCallback (token) {
console.log('cancelling callback token ' + token)
if (this.callstack[token] !== undefined) {
if (this.callstack[token].timeout !== undefined) {
clearTimeout(this.callstack[token].timeout)
}
}
delete this.callstack[token]
}
send (msg) {
let buffer, dataview, idx
buffer = new ArrayBuffer(lc.HEADER_LENGTH + msg.len * 4)
dataview = new DataView(buffer)
if (msg.data !== undefined && msg.len > 0) {
if (typeof msg.data === 'object') {
// copy ArrayBuffer into new buffer with space for header data
idx = lc.HEADER_LENGTH + msg.len
const tmp = new Uint8Array(idx)
tmp.set(new Uint8Array(msg.data), lc.HEADER_LENGTH)
buffer = tmp.buffer
dataview = new DataView(buffer)
} else {
// string data, convert to UTF-8
idx = util.convertUTF16toUTF8(lc.HEADER_LENGTH, msg.data, msg.len, dataview)
}
}
// write headers
dataview.setUint8(0, msg.opcode)
dataview.setUint32(1, idx - lc.HEADER_LENGTH)
dataview.setUint32(5, msg.id)
dataview.setUint32(9, msg.id2)
dataview.setUint32(13, msg.token)
this.websocket.send(buffer)
};
wsClose (e) {
console.log('websocket close: (' + e.code + ') ' + e.reason)
console.log('websocket.readyState: ' + this.websocket.readyState)
console.log('reinitializing websocket')
this.connect()
};
wsError (e) {
console.log('websocket error' + e.message)
console.log('websocket.readyState: ' + this.websocket.readyState)
};
wsMessage (msg) {
console.log('websocket message received (type=' + msg.type + ')')
if (typeof (msg) === 'object' && msg.data instanceof ArrayBuffer) {
const dataview = new DataView(msg.data)
const cmsg = new lc.Message(msg.data)
cmsg.opcode = dataview.getUint8(0)
cmsg.len = dataview.getUint32(1)
cmsg.id = dataview.getUint32(5)
cmsg.id2 = dataview.getUint32(9)
cmsg.token = dataview.getUint32(13)
cmsg.payload = msg.data.slice(lc.HEADER_LENGTH)
console.log('opcode: ' + cmsg.opcode)
console.log('len: ' + cmsg.len)
console.log('id: ' + cmsg.id)
console.log('id2: ' + cmsg.id2)
console.log('token: ' + cmsg.token)
if (this.callstack[cmsg.token] !== undefined) {
this.callstack[cmsg.token].updated = Date.now()
cmsg.sent = this.callstack[cmsg.token].created
cmsg.recv = this.callstack[cmsg.token].updated
cmsg.delay = cmsg.recv - cmsg.sent
console.log('message reponse took ' + cmsg.delay + ' ms')
if (this.callstack[cmsg.token].timeout !== undefined) {
console.log('clearing callback timer ' + cmsg.token)
clearTimeout(this.callstack[cmsg.token].timeout)
this.callstack[cmsg.token].timeout = undefined
}
if (this.callstack[cmsg.token].resolve !== undefined) {
this.callstack[cmsg.token].resolve(cmsg)
if (this.callstack[cmsg.token] !== undefined) {
if (!this.callstack[cmsg.token].repeat) {
this.cancelCallback(cmsg.token)
}
}
}
}
}
}
wsOpen (e) {
console.log('websocket open')
console.log('websocket.readyState: ' + this.websocket.readyState)
this.resolveconnect()
};
}
// eslint-disable-next-line no-unused-vars
function LibrecastException (errorCode) {
this.code = errorCode
this.name = lc.ErrorMsg[errorCode]
this.errormsg = 'ERROR (' + this.code + ') ' + this.name
}
const lc = {}
// default op callback timeout in ms
lc.DEFAULT_TIMEOUT = 5000
lc.NO_TIMEOUT = -1
lc.WS_CONNECTING = 0
lc.WS_OPEN = 1
lc.WS_CLOSING = 2
lc.WS_CLOSED = 3
lc.OP_NOOP = 0x01
lc.OP_SETOPT = 0x02
lc.OP_SOCKET_NEW = 0x03
lc.OP_SOCKET_GETOPT = 0x04
lc.OP_SOCKET_SETOPT = 0x05
lc.OP_SOCKET_LISTEN = 0x06
lc.OP_SOCKET_IGNORE = 0x07
lc.OP_SOCKET_CLOSE = 0x08
lc.OP_SOCKET_MSG = 0x09
lc.OP_CHANNEL_NEW = 0x0a
lc.OP_CHANNEL_GETMSG = 0x0b
lc.OP_CHANNEL_GETOPT = 0x0c
lc.OP_CHANNEL_SETOPT = 0x0d
lc.OP_CHANNEL_GETVAL = 0x0e
lc.OP_CHANNEL_SETVAL = 0x0f
lc.OP_CHANNEL_BIND = 0x10
lc.OP_CHANNEL_UNBIND = 0x11
lc.OP_CHANNEL_JOIN = 0x12
lc.OP_CHANNEL_PART = 0x13
lc.OP_CHANNEL_SEND = 0x14
lc.HEADER_LENGTH = 25
lc.ERR_SUCCESS = 0
lc.ERR_FAILURE = 1
lc.ERR_WEBSOCKET_UNSUPPORTED = 2
lc.ERR_WEBSOCKET_NOTREADY = 3
lc.ERR_CALLBACK_NOT_FUNCTION = 4
lc.ERR_MISSING_ARG = 5
lc.ErrorMsg = {}
lc.ErrorMsg[lc.ERR_SUCCESS] = 'Success'
lc.ErrorMsg[lc.ERR_FAILURE] = 'Failure'
lc.ErrorMsg[lc.ERR_WEBSOCKET_UNSUPPORTED] = 'Browser does not support websockets'
lc.ErrorMsg[lc.ERR_WEBSOCKET_NOTREADY] = 'Websocket not ready'
lc.ErrorMsg[lc.ERR_CALLBACK_NOT_FUNCTION] = 'Callback not a function'
lc.ErrorMsg[lc.ERR_MISSING_ARGUMENT] = 'Required argument is missing'
// eslint-disable-next-line no-unused-vars
const Message = class {
constructor (data) {
this.opcode = lc.OP_NOOP
this.data = data
if (data instanceof ArrayBuffer) {
this.len = data.byteLength
} else {
this.len = (this.data === undefined) ? 0 : data.length
}
this.id = 0
this.id2 = 0
this.token = 0
};
get utf8 () {
if (this.data !== undefined) {
const decoder = new TextDecoder('utf-8')
return decoder.decode(new Uint8Array(this.data)).slice(lc.HEADER_LENGTH)
}
}
}
// eslint-disable-next-line no-unused-vars
const Socket = class {
constructor (lctx) {
console.log('Socket constructor')
if (lctx === undefined) throw new Error('Librecast.Context required')
this.lctx = lctx
this.id = undefined
this.oncreate = new Promise((resolve, reject) => {
const msg = new lc.Message()
msg.opcode = lc.OP_SOCKET_NEW
msg.token = this.lctx.callback(resolve, reject)
this.lctx.send(msg)
})
.then((msg) => {
this.id = msg.id
})
};
op (opcode, data, timeout, callback) {
return new Promise((resolve, reject) => {
if (this.lctx.websocket.readyState === lc.WS_OPEN) {
const msg = new lc.Message(data)
msg.opcode = opcode
msg.id = this.id
if (callback !== false) {
msg.token = this.lctx.callback(resolve, reject, timeout)
}
this.lctx.send(msg)
} else {
reject(LibrecastException(lc.ERR_WEBSOCKET_NOTREADY))
}
})
}
close () {
this.lctx.cancelCallback(this.token)
return this.op(lc.OP_SOCKET_CLOSE, undefined, undefined, false)
}
listen (onmessage, onerror) {
console.log('listening on socket ' + this.id)
if (this.lctx.websocket.readyState === lc.WS_OPEN) {
const msg = new lc.Message()
msg.opcode = lc.OP_SOCKET_LISTEN
msg.id = this.id
msg.token = this.lctx.callback(onmessage, onerror, lc.NO_TIMEOUT, true)
this.token = msg.token
this.lctx.send(msg)
} else {
onerror(LibrecastException(lc.ERR_WEBSOCKET_NOTREADY))
}
}
}
'use strict'
const convertUTF16toUTF8 = function (idx, utf16in, len, dataview) {
let c, i
for (i = 0; i < len; i++) {
c = utf16in.charCodeAt(i)
if (c <= 0x7f) {
dataview.setUint8(idx++, c)
} else if (c <= 0x7ff) {
dataview.setUint8(idx++, 0xc0 | (c >>> 6))
dataview.setUint8(idx++, 0x80 | (c & 0x3f))
} else if (c <= 0xffff) {
dataview.setUint8(idx++, 0xe0 | (c >>> 12))
dataview.setUint8(idx++, 0x80 | ((c >>> 6) & 0x3f))
dataview.setUint8(idx++, 0x80 | (c & 0x3f))
} else {
console.log('UTF-16 surrogate pair, ignoring')
/* TODO: 4 byte UTF-8 encoding, just in case anyone speaks Vogon */
}
}
return idx
}
/* return bytes required to 7bit encode length */
const bytes7BitLength = function (input) {
const buffer = new ArrayBuffer(4)
const view = new DataView(buffer, 0)
let bytes = 1
view.setUint32(0, input, true)
for (let i = 0, n = view.getUint32(0, true); n > 0x7f; n >>>= 7) {
view.setUint8(i++, 0x80 | n)
bytes++
}
return bytes
}
const encode7BitLength = function (input) {
const bytes = bytes7BitLength(input)
let idx = 0; let n
const buffer = new ArrayBuffer(bytes)
const view = new DataView(buffer)
view.setUint8(idx, input, true)
for (n = view.getUint8(idx); n > 0x7f; n >>>= 7) {
view.setUint8(idx++, 0x80 | n)
}
view.setUint8(idx++, n)
return buffer
}
const bytesRequired = function (fields) {
let bytes = 0
for (let i = 0; i < fields.length; i++) {
if (fields[i] !== null) {
bytes += bytes7BitLength(fields[i].length) + fields[i].length
}
}
return bytes
}
const wirePack7Bit = function (fields, bytes, offset) {
let idx = offset
const buffer = new ArrayBuffer(bytes)
const uint8 = new Uint8Array(buffer)
const view = new DataView(buffer)
for (let i = 0; i < fields.length; i++) {
if (fields[i] !== null) {
let n
const len = fields[i].length
view.setUint8(idx, len)
for (n = view.getUint8(idx); n > 0x7f; n >>>= 7) {
view.setUint8(idx++, 0x80 | n)
}
view.setUint8(idx++, n)
if (typeof fields[i] === 'object') {
uint8.set(new Uint8Array(fields[i]), idx)
idx += len
} else { idx = convertUTF16toUTF8(idx, fields[i], len, view) }
}
}
return buffer
}
const wirePackPre = function (pre, fields) {
const offset = pre.length
const bytes = bytesRequired(fields) + offset
const buffer = wirePack7Bit(fields, bytes, offset)
const uint8 = new Uint8Array(buffer)
uint8.set(pre)
return buffer
}
const wirePack = function (opcode, flags, fields) {
return wirePackPre([opcode, flags], fields)
}
const wireUnpack7Bit = function (buffer, offset) {
if (offset === undefined) offset = 0
const fields = []
const view = new DataView(buffer)
const bytes = buffer.byteLength
for (let i = offset, len = 0; i < bytes; i += len) {
let n = 0; let shift = 0
let b
do {
if (i >= bytes) throw new Error('overflow')
b = view.getUint8(i++)
n |= (b & 0x7f) << shift
shift += 7
} while (b & 0x80)
len = n /* FIXME: convert to host byte order */
if (i + len > bytes) break // throw "out of bounds";
fields.push(new Uint8Array(buffer.slice(i, i + len)))
}
return fields
}
const wireUnpack = function (buffer) {
const view = new DataView(buffer)
const opcode = view.getUint8(0)
const flags = view.getUint8(1)
const fields = wireUnpack7Bit(buffer, 2)
return [opcode, flags, fields]
}
const keysEqual = function (key1, key2) {
const len1 = key1.byteLength
const len2 = key2.byteLength
if (len1 !== len2) return false
for (let i = 0; i < len1; i++) {
if (key1[i] !== key2[i]) return false
}
return true
}
// eslint-disable-next-line no-unused-vars
const util = {
bytes7BitLength,
convertUTF16toUTF8,
encode7BitLength,
keysEqual,
wirePack,
wirePackPre,
wirePack7Bit,
wireUnpack,
wireUnpack7Bit
}
lc.Context = Context
lc.Socket = Socket
lc.Channel = Channel
lc.Message = Message
lc.util = util
return lc;
}());