UNPKG

librecast-live

Version:

Live Streaming Video Platform with IPv6 Multicast

523 lines (476 loc) 15.3 kB
/* * 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; }());