@ceeblue/videojs-plugins
Version:
Videojs set of plugins for playing streams from the Ceeblue cloud. Supports WebRTC and WHEP, video quality switching, protocol fallbacks, and more.
1,661 lines (1,654 loc) • 208 kB
JavaScript
/*! @name @ceeblue/videojs-plugins @version 1.2.0 @license AGPL-3.0-or-later */
import _extends from '@babel/runtime/helpers/extends';
import videojs from 'video.js';
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol */
function __awaiter$1(thisArg, _arguments, P, generator) {
function adopt(value) {
return value instanceof P ? value : new P(function (resolve) {
resolve(value);
});
}
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
}
function rejected(value) {
try {
step(generator["throw"](value));
} catch (e) {
reject(e);
}
}
function step(result) {
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
}
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
}; /**
* Copyright 2024 Ceeblue B.V.
* This file is part of https://github.com/CeeblueTV/web-utils which is released under GNU Affero General Public License.
* See file LICENSE or go to https://spdx.org/licenses/AGPL-3.0-or-later.html for full license details.
*/
/**
* BinaryReader allows to read binary data
*/
const _decoder$1 = new TextDecoder();
class BinaryReader {
constructor(data) {
this._data = 'buffer' in data ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength) : new Uint8Array(data);
this._size = this._data.byteLength;
this._position = 0;
this._view = new DataView(this._data.buffer, this._data.byteOffset, this._size);
}
data() {
return this._data;
}
size() {
return this._size;
}
available() {
return this._size - this._position;
}
value(position = this._position) {
return this._data[position];
}
position() {
return this._position;
}
reset(position = 0) {
this._position = Math.max(0, position > this._size ? this._size : position);
}
shrink(available) {
const rest = this._size - this._position;
if (available > rest) {
return rest;
}
this._size = this._position + available;
return available;
}
next(count = 1) {
const rest = this._size - this._position;
if (count > rest) {
count = rest;
}
this._position = Math.max(0, this._position + count);
return count;
}
read8() {
return this.next(1) === 1 ? this._view.getUint8(this._position - 1) : 0;
}
read16() {
return this.next(2) === 2 ? this._view.getUint16(this._position - 2) : 0;
}
read24() {
return this.next(3) === 3 ? this._view.getUint16(this._position - 3) << 8 | this._view.getUint8(this._position - 1) & 0xff : 0;
}
read32() {
return this.next(4) === 4 ? this._view.getUint32(this._position - 4) : 0;
}
read64() {
if (this.next(8) !== 8) {
return 0;
}
return this._view.getUint32(this._position - 8) * 4294967296 + this._view.getUint32(this._position - 4);
}
readFloat() {
return this.next(4) === 4 ? this._view.getFloat32(this._position - 4) : 0;
}
readDouble() {
return this.next(8) === 8 ? this._view.getFloat64(this._position - 8) : 0;
}
read7Bit(bytes = 5) {
let result = 0;
let factor = 1;
while (this.available()) {
const byte = this.read8();
result += (byte & 0x7f) * factor;
if (!(byte & 0x80)) {
break;
}
factor *= 128;
}
return result;
}
readString() {
let i = this._position;
while (i < this._size && this._data[i]) {
++i;
}
const result = this.read(i - this._position);
this.next(); // skip the 0 termination
return _decoder$1.decode(result);
}
readHex(size) {
let hex = '';
while (size-- > 0) {
hex += ('0' + this.read8().toString(16)).slice(-2);
}
return hex;
}
/**
* Read bytes, to convert bytes to string use String.fromCharCode(...reader.read(size)) or Util.stringify
* @param {UInt32} size
*/
read(size = this.available()) {
if (this.available() < size) {
return new Uint8Array(size); // default value = empty bytearray!
}
const pos = this._position;
return this._data.subarray(pos, Math.max(pos, this._position += size));
}
} /**
* Copyright 2024 Ceeblue B.V.
* This file is part of https://github.com/CeeblueTV/web-utils which is released under GNU Affero General Public License.
* See file LICENSE or go to https://spdx.org/licenses/AGPL-3.0-or-later.html for full license details.
*/
const _encoder$1 = new TextEncoder();
/**
* BinaryWriter allows to write data in its binary form
*/
class BinaryWriter {
get view() {
if (!this._view) {
this._view = new DataView(this._data.buffer, this._data.byteOffset, this._data.byteLength);
}
return this._view;
}
get capacity() {
return this._data.byteLength;
}
constructor(dataOrSize = 64, offset = 0, length) {
if (typeof dataOrSize == 'number') {
// allocate new buffer
this._data = new Uint8Array(dataOrSize);
this._size = 0;
} else if ('buffer' in dataOrSize) {
// append to existing data!
this._data = new Uint8Array(dataOrSize.buffer, dataOrSize.byteOffset, dataOrSize.byteLength);
this._size = dataOrSize.byteLength;
} else {
// overrides data
this._isConst = true; // better than boolean for memory usage
if (length == null) {
// /!\ Safari does not support undefined length, so we need to use byteLength instead
length = dataOrSize.byteLength;
}
this._data = new Uint8Array(dataOrSize, offset, length);
this._size = 0;
}
}
data() {
return new Uint8Array(this._data.buffer, this._data.byteOffset, this._size);
}
size() {
return this._size || 0;
}
next(count = 1) {
return this.reserve(this._size += count);
}
clear(size = 0) {
return this.reserve(this._size = size);
}
/**
* Write binary data
* @param data
*/
write(data) {
this.reserve(this._size + data.length);
this._data.set(data, this._size);
this._size += data.length;
return this;
}
write8(value) {
if (value > 0xff) {
// cast to 8bits range
value = 0xff;
}
this.reserve(this._size + 1);
this._data[this._size++] = value;
return this;
}
write16(value) {
if (value > 0xffff) {
// cast to 16bits range
value = 0xffff;
}
this.reserve(this._size + 2);
this.view.setUint16(this._size, value);
this._size += 2;
return this;
}
write24(value) {
if (value > 0xffffff) {
// cast to 24bits range
value = 0xffffff;
}
this.reserve(this._size + 3);
this.view.setUint16(this._size, value >> 8);
this.view.setUint8(this._size += 2, value & 0xff);
++this._size;
return this;
}
write32(value) {
if (value > 0xffffffff) {
// cast to 32bits range
value = 0xffffffff;
}
this.reserve(this._size + 4);
this.view.setUint32(this._size, value);
this._size += 4;
return this;
}
write64(value) {
this.write32(value / 4294967296);
return this.write32(value & 0xffffffff);
}
writeFloat(value) {
this.reserve(this._size + 4);
this.view.setFloat32(this._size, value);
this._size += 4;
return this;
}
writeDouble(value) {
this.reserve(this._size + 8);
this.view.setFloat64(this._size, value);
this._size += 8;
return this;
}
write7Bit(value) {
let byte = value & 0x7f;
while (value = Math.floor(value / 0x80)) {
// equivalent to >>=7 for JS!
this.write8(0x80 | byte);
byte = value & 0x7f;
}
return this.write8(byte);
}
writeString(value) {
return this.write(_encoder$1.encode(value)).write8(0);
}
writeHex(value) {
for (let i = 0; i < value.length; i += 2) {
this.write8(parseInt(value.substring(i, i + 2), 16));
}
return this;
}
reserve(size) {
if (!this._data) {
throw Error('buffer not writable');
}
if (size <= this._data.byteLength) {
return this;
}
if (this._isConst) {
throw Error('writing exceeds maximum ' + this._data.byteLength + ' bytes limit');
}
--size;
size |= size >> 1;
size |= size >> 2;
size |= size >> 4;
size |= size >> 8;
size |= size >> 16;
++size;
const data = new Uint8Array(size);
data.set(this._data); // copy old buffer!
this._data = data;
this._view = undefined; // release view
return this;
}
} /**
* Copyright 2024 Ceeblue B.V.
* This file is part of https://github.com/CeeblueTV/web-utils which is released under GNU Affero General Public License.
* See file LICENSE or go to https://spdx.org/licenses/AGPL-3.0-or-later.html for full license details.
*/
/**
* BitReader allows to read binary data bit by bit
*/
class BitReader {
constructor(data) {
if ('buffer' in data) {
this._data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
} else {
this._data = new Uint8Array(data);
}
this._size = this._data.byteLength;
this._position = 0;
this._bit = 0;
}
data() {
return this._data;
}
size() {
return this._size;
}
available() {
return (this._size - this._position) * 8 - this._bit;
}
next(count = 1) {
let gotten = 0;
while (this._position !== this._size && count--) {
++gotten;
if (++this._bit === 8) {
this._bit = 0;
++this._position;
}
}
return gotten;
}
read(count = 1) {
let result = 0;
while (this._position !== this._size && count--) {
result <<= 1;
if (this._data[this._position] & 0x80 >> this._bit++) {
result |= 1;
}
if (this._bit === 8) {
this._bit = 0;
++this._position;
}
}
return result;
}
read8() {
return this.read(8);
}
read16() {
return this.read(16);
}
read24() {
return this.read(24);
}
read32() {
return this.read(32);
}
readExpGolomb() {
let i = 0;
while (!this.read()) {
if (!this.available()) {
return 0;
}
++i;
}
const result = this.read(i);
if (i > 15) {
console.warn('Exponential-Golomb code exceeding unsigned 16 bits');
return 0;
}
return result + (1 << i) - 1;
}
} /******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) {
return value instanceof P ? value : new P(function (resolve) {
resolve(value);
});
}
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
}
function rejected(value) {
try {
step(generator["throw"](value));
} catch (e) {
reject(e);
}
}
function step(result) {
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
}
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
}; /**
* Copyright 2024 Ceeblue B.V.
* This file is part of https://github.com/CeeblueTV/web-utils which is released under GNU Affero General Public License.
* See file LICENSE or go to https://spdx.org/licenses/AGPL-3.0-or-later.html for full license details.
*/
const _decoder = new TextDecoder();
const _encoder = new TextEncoder();
/* eslint-disable @typescript-eslint/no-explicit-any */
const _perf = performance; // to increase x10 now performance!
/**
* Some basic utility functions
*/
/**
* An empty lambda function, pratical to disable default behavior of function or events which are not expected to be null
* @example
* console.log = Util.EMPTY_FUNCTION; // disable logs without breaking calls
*/
const EMPTY_FUNCTION = () => {};
/**
* Efficient and high resolution timestamp in milliseconds elapsed since {@link Util.timeOrigin}
*/
function time() {
return Math.floor(_perf.now());
}
/**
* Time origin represents the time when the application has started
*/
function timeOrigin() {
return Math.floor(_perf.now() + _perf.timeOrigin);
}
/**
* Parse query and returns it in an easy-to-use Javascript object form
* @param urlOrQueryOrSearch string, url, or searchParams containing query. If not set it uses `location.search` to determinate query.
* @returns An javascript object containing each option
*/
function options(urlOrQueryOrSearch = typeof location === 'undefined' ? undefined : location) {
if (!urlOrQueryOrSearch) {
return {};
}
try {
const url = urlOrQueryOrSearch;
urlOrQueryOrSearch = new URL(url).searchParams;
} catch (e) {
if (typeof urlOrQueryOrSearch == 'string') {
if (urlOrQueryOrSearch.startsWith('?')) {
urlOrQueryOrSearch = urlOrQueryOrSearch.substring(1);
}
urlOrQueryOrSearch = new URLSearchParams(urlOrQueryOrSearch);
}
}
// works same if urlOrQueryOrSearch is null, integer, or a already object etc...
return objectFrom(urlOrQueryOrSearch, {
withType: true,
noEmptyString: true
});
}
/**
* Returns an easy-to-use Javascript object something iterable, such as a Map, Set, or Array
* @param value iterable input
* @param params.withType `false`, if set it tries to cast string value to a JS number/boolean/undefined/null type.
* @param params.noEmptyString `false`, if set it converts empty string value to a true boolean, usefull to allow a `if(result.key)` check for example
* @returns An javascript object
*/
function objectFrom(value, params) {
params = _extends({
withType: false,
noEmptyString: false
}, params);
const obj = {};
if (!value) {
return obj;
}
for (const [key, val] of objectEntries(value)) {
value = val;
if (params.withType && value != null && value.substring) {
if (value) {
const number = Number(value);
if (isNaN(number)) {
switch (value.toLowerCase()) {
case 'true':
value = true;
break;
case 'false':
value = false;
break;
case 'null':
value = null;
break;
case 'undefined':
value = undefined;
break;
}
} else {
value = number;
}
} else if (params.noEmptyString) {
// if empty string => TRUE to allow a if(options.key) check for example
value = true;
}
}
if (obj[key]) {
if (!Array.isArray(obj[key])) {
obj[key] = new Array(obj[key]);
}
obj[key].push(value);
} else {
obj[key] = value;
}
}
return obj;
}
/**
* Returns entries from something iterable, such as a Map, Set, or Array
* @param value iterable input
* @returns An javascript object
*/
function objectEntries(value) {
if (value.entries) {
return value.entries();
}
return Array.from({
[Symbol.iterator]: function* () {
for (const key in value) {
yield [key, value[key]];
}
}
});
}
/**
* Converts various data types, such as objects, strings, exceptions, errors,
* or numbers, into a string representation. Since it offers a more comprehensive format,
* this function is preferred to `JSON.stringify()`.
* @param obj Any objects, strings, exceptions, errors, or number
* @param params.space `''`, allows to configure space in the string representation
* @param params.decimal `2`, allows to choose the number of decimal to display in the string representation
* @param params.recursion `1`, recursion depth to stringify recursively every object value until this depth, beware if a value refers to a already parsed value an infinite loop will occur
* @param params.noBin `false`, when set skip binary encoding and write inplace a bin-length information
* @returns the final string representation
*/
// Online Javascript Editor for free
// Write, Edit and Run your Javascript code using JS Online Compiler
function stringify(obj, params = {}) {
params = _extends({
space: ' ',
decimal: 2,
recursion: 1,
noBin: false
}, params);
if (obj == null) {
return String(obj);
}
const error = obj.error || obj.message;
if (error) {
// is a error!
obj = error;
}
// number
if (obj.toFixed) {
return obj.toFixed(Number(params.decimal) || 0);
}
if (obj.byteLength != null && (obj === null || obj === void 0 ? void 0 : obj[Symbol.iterator])) {
// Binary!
if (!params.noBin) {
return _decoder.decode(obj);
}
return '[' + obj.byteLength + '#bytes]';
}
// boolean or string type or stop recursivity
if (typeof obj === 'boolean' || obj.substring || !params.recursion) {
// is already a string OR has to be stringified
return String(obj);
}
const space = params.space || '';
if (Array.isArray(obj)) {
// Array!
let res = '';
for (const value of obj) {
res += (res ? ',' : '[') + space;
res += stringify(value, _extends(_extends({}, params), {
recursion: params.recursion - 1
}));
}
return res += space + ']';
}
let res = '';
for (const name in obj) {
res += (res ? ',' : '{') + space + name + ':';
res += stringify(obj[name], _extends(_extends({}, params), {
recursion: params.recursion - 1
}));
}
return res += space + '}';
}
/**
* Encode a string to a binary representation
* @param value string value to convert
* @returns binary conversion
*/
function toBin(value) {
return _encoder.encode(value);
}
/**
* Execute a promise in a safe way with a timeout if caller doesn't resolve it in the accurate time
*/
function safePromise(timeout, promise) {
// Returns a race between our timeout and the passed in promise
let timer;
return Promise.race([promise instanceof Promise ? promise : new Promise(promise), new Promise((resolve, reject) => timer = setTimeout(() => reject('timed out in ' + timeout + 'ms'), timeout))]).finally(() => clearTimeout(timer));
}
/**
* Wait in milliseconds, requires a call with await keyword!
*/
function sleep(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
/**
* Test equality between two value whatever their type, array included
*/
function equal(a, b) {
if (Object(a) !== a) {
if (Object(b) === b) {
return false;
}
// both primitive (null and undefined included)
return a === b;
}
// complexe object
if (a[Symbol.iterator]) {
if (!b[Symbol.iterator]) {
return false;
}
if (a.length !== b.length) {
return false;
}
for (let i = 0; i !== a.length; ++i) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
return a === b;
}
/**
* fetch help method with few usefull fix:
* - throw an string exception if response code is not 200 with the text of the response or uses statusText
*/
function fetch$1(input, init) {
return __awaiter(this, void 0, void 0, function* () {
const response = yield self.fetch(input, init);
if (response.status >= 300) {
let error;
if (response.body) {
error = yield response.text();
}
throw (error || response.statusText || response.status).toString();
}
return response;
});
}
/**
* Get Extension part from path
* @param path path to parse
* @returns the extension
*/
function getExtension(path) {
const dot = path.lastIndexOf('.');
const ext = dot >= 0 && dot > path.lastIndexOf('/') ? path.substring(dot) : '';
return ext;
}
/**
* Get File part from path
* @param path path to parse
* @returns the file name
*/
function getFile(path) {
return path.substring(path.lastIndexOf('/') + 1);
}
/**
* Get Base File part from path, without extension
* @param path path to parse
* @returns the base file name
*/
function getBaseFile(path) {
const dot = path.lastIndexOf('.');
const file = path.lastIndexOf('/') + 1;
return dot >= 0 && dot >= file ? path.substring(file, dot) : path.substring(file);
}
function codesFromString(value) {
const codes = [];
for (let i = 0; i < value.length; ++i) {
codes.push(value.charCodeAt(i));
}
return codes;
}
/**
* String Trim function with customizable chars
* @param value string to trim
* @param chars chars to use to trim
* @returns string trimmed
*/
function trim(value, chars = ' ') {
const codes = codesFromString(chars);
let start = 0;
while (start < value.length && codes.includes(value.charCodeAt(start))) {
++start;
}
let end = value.length;
while (end > 0 && codes.includes(value.charCodeAt(end - 1))) {
--end;
}
return value.substring(start, end);
}
/**
* String Trim Start function with customizable chars
* @param value string to trim start
* @param chars chars to use to trim start
* @returns string trimmed
*/
function trimStart(value, chars = ' ') {
const codes = codesFromString(chars);
let i = 0;
while (i < value.length && codes.includes(value.charCodeAt(i))) {
++i;
}
return value.substring(i);
}
/**
* String Trim End function with customizable chars
* @param value string to trim end
* @param chars chars to use to trim end
* @returns string trimmed
*/
function trimEnd(value, chars = ' ') {
const codes = codesFromString(chars);
let i = value.length;
while (i > 0 && codes.includes(value.charCodeAt(i - 1))) {
--i;
}
return value.substring(0, i);
}
var Util$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
EMPTY_FUNCTION: EMPTY_FUNCTION,
equal: equal,
fetch: fetch$1,
getBaseFile: getBaseFile,
getExtension: getExtension,
getFile: getFile,
objectEntries: objectEntries,
objectFrom: objectFrom,
options: options,
safePromise: safePromise,
sleep: sleep,
stringify: stringify,
time: time,
timeOrigin: timeOrigin,
toBin: toBin,
trim: trim,
trimEnd: trimEnd,
trimStart: trimStart
}); /**
* Copyright 2024 Ceeblue B.V.
* This file is part of https://github.com/CeeblueTV/web-utils which is released under GNU Affero General Public License.
* See file LICENSE or go to https://spdx.org/licenses/AGPL-3.0-or-later.html for full license details.
*/
/**
* Compute ByteRate every delta time
*/
class ByteRate {
onBytes(bytes) {}
get delta() {
return this._delta;
}
constructor(delta = 1000) {
this._time = time();
this._value = NaN;
this._delta = delta;
this._bytes = 0;
}
value() {
return Math.round(this.exact());
}
exact() {
const now = time();
const elapsed = now - this._time;
if (elapsed > this._delta || isNaN(this._value)) {
// wait "_delta" before next compute rate
this._value = this._bytes * 1000 / elapsed;
this._bytes = 0;
this._time = now;
}
return this._value;
}
addBytes(bytes) {
this._bytes += bytes;
this.onBytes(bytes);
return this;
}
} /**
* Copyright 2024 Ceeblue B.V.
* This file is part of https://github.com/CeeblueTV/web-utils which is released under GNU Affero General Public License.
* See file LICENSE or go to https://spdx.org/licenses/AGPL-3.0-or-later.html for full license details.
*/
/**
* Help class to manipulate and parse a net address. The Address can be only the domain field,
* or a URL format with protocol and path part `(http://)domain(:port/path)`
* @example
* const address = new Address('nl-ams-42.live.ceeblue.tv:80');
* console.log(address.domain) // 'nl-ams-42.live.ceeblue.tv'
* console.log(address.port) // '80'
* console.log(address) // 'nl-ams-42.live.ceeblue.tv:80'
*/
class NetAddress$1 {
/**
* Static help function to build an end point from an address `(proto://)domain(:port/path)`
*
* Mainly it fix the protocol, in addition if:
* - the address passed is securized (TLS) and protocol is not => it tries to fix protocol to get its securize version
* - the address passed is non securized and protocol is (TLS) => it tries to fix protocol to get its unsecurized version
* @param protocol protocol to set in the end point returned
* @param address string address to fix with protocol as indicated
* @returns the end point built
* @example
* console.log(NetAddress.fixProtocol('ws','http://domain/path')) // 'ws://domain/path'
* console.log(NetAddress.fixProtocol('ws','https://domain/path')) // 'wss://domain/path'
* console.log(NetAddress.fixProtocol('wss','http://domain/path')) // 'ws://domain/path'
*/
static fixProtocol(protocol, address) {
const found = address.indexOf('://');
// isolate protocol is present in address
if (found >= 0) {
// In this case replace by protocol in keeping SSL like given in address
if (found > 2 && address.charAt(found - 1).toLowerCase() === 's') {
// SSL!
if (protocol.length <= 2 || !protocol.endsWith('s')) {
protocol += 's'; // Add SSL
}
} else {
// Not SSL!
if (protocol.length > 2 && protocol.endsWith('s')) {
protocol = protocol.slice(0, -1); // Remove SSL
}
}
// Build address!
address = address.substring(found + 3);
}
return protocol + '://' + address;
}
/**
* The domain part from address `(http://)domain(:port/path)`
*/
get domain() {
return this._domain;
}
/**
* The port part from address `(http://)domain(:port/path)`, or defaultPort if passed in NetAddress constructor
*/
get port() {
return this._port;
}
/**
* @returns the string address as passed in the constructor
*/
toString() {
return this._address;
}
/**
* @returns the string address as passed in the constructor
* @override
*/
valueOf() {
return this._address;
}
/**
* Build a NetAddress object and parse address
* @param address string address to parse, accept an url format with protocol and path `(http://)domain(:port/path)`
* @param defaultPort set a default port to use if there is no port in the string address parsed
*/
constructor(address, defaultPort) {
this._address = address;
// Remove Protocol
let pos = address.indexOf('/');
if (pos >= 0) {
// Remove ://
if (address.charCodeAt(pos + 1) === 47) {
// has //
if (pos > 0) {
if (address.charCodeAt(pos - 1) === 58) {
// has ://
address = address.substring(pos + 2);
} // something else #//
} else {
// otherwise starts by //
address = address.substring(2);
}
} else if (!pos) {
// starts by /, remove it
address = address.substring(1);
} // else something else #/
}
this._domain = address;
this._port = defaultPort;
// Parse Port
pos = address.lastIndexOf(':');
if (pos >= 0) {
const port = parseInt(address.substring(pos + 1));
if (port && port <= 0xffff) {
this._port = port;
this._domain = address.substring(0, pos);
}
} else {
// Remove Path!
pos = address.indexOf('/');
if (pos >= 0) {
this._domain = address.substring(0, pos);
}
}
}
} /**
* Copyright 2024 Ceeblue B.V.
* This file is part of https://github.com/CeeblueTV/web-utils which is released under GNU Affero General Public License.
* See file LICENSE or go to https://spdx.org/licenses/AGPL-3.0-or-later.html for full license details.
*/
/**
* Type of connection
*/
var Type;
(function (Type) {
Type["HESP"] = "HESP";
Type["WRTS"] = "WebRTS";
Type["WEBRTC"] = "WebRTC";
Type["META"] = "Meta";
Type["DATA"] = "Data";
})(Type || (Type = {}));
/**
* Some connection utility functions
*/
/**
* Defines the {@link Params.mediaExt} based on the type of parameters and its endpoint.
* This method always assigns a value to params.mediaExt, defaulting to an empty string if indeterminable,
* allowing detection of whether the function has been applied to the parameters.
* @param type The type of parameters to define.
* @param params The parameters for which the media extension is to be defined
*/
function defineMediaExt(type, params) {
// Compute appropriate mediaExt out parameter
switch (type) {
case Type.HESP:
params.mediaExt = 'mp4';
break;
case Type.WEBRTC:
params.mediaExt = 'rtp';
break;
case Type.WRTS:
{
try {
const url = new URL(params.endPoint);
const ext = getExtension(getFile(url.pathname));
// set extension just if not json, json means a manifest file endPoint
if (ext && ext.toLowerCase() !== '.json') {
params.mediaExt = ext;
}
} catch (_) {
// not an URL, it's only a host => keep mediaExt unchanged to build the URL
}
if (!params.mediaExt) {
// set to its default rts value => always set for WRTS!
params.mediaExt = 'rts';
}
break;
}
case Type.META:
params.mediaExt = 'js';
break;
case Type.DATA:
params.mediaExt = 'json';
break;
default:
params.mediaExt = ''; // set always a value to know that the parameters have been fixed
console.warn('Unknown params type ' + type);
break;
}
// Fix mediaExt in removing the possible '.' prefix
trimStart(params.mediaExt, '.');
}
/**
* Build an URL from {@link Type | type} and {@link Params | params}
* @param type Type of the connection wanted
* @param params Connection parameters
* @param protocol Optional parameter to choose the prefered protocol to connect
* @returns The URL of connection
*/
function buildURL(type, params, protocol = 'wss') {
defineMediaExt(type, params);
const url = new URL(NetAddress$1.fixProtocol(protocol, params.endPoint));
if (url.pathname.length <= 1) {
// build ceeblue path!
switch (type) {
case Type.HESP:
url.pathname = '/hesp/' + params.streamName + '/index.json';
break;
case Type.WEBRTC:
url.pathname = '/webrtc/' + params.streamName;
break;
case Type.WRTS:
url.pathname = '/wrts/' + params.streamName + '.' + params.mediaExt;
break;
case Type.META:
url.pathname = '/json_' + params.streamName + '.js';
break;
case Type.DATA:
url.pathname = '/' + params.streamName + '.json';
break;
default:
console.warn('Unknown url type ' + type);
break;
}
} // Host has already a path! keep it unchanged, it's user intentionnal (used with some other WHIP/WHEP server?)
if (params.accessToken) {
url.searchParams.set('id', params.accessToken);
}
for (const key in params.query) {
url.searchParams.set(key, params.query[key]);
}
return url;
}
var Connect$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
get Type() {
return Type;
},
buildURL: buildURL,
defineMediaExt: defineMediaExt
}); /**
* Copyright 2024 Ceeblue B.V.
* This file is part of https://github.com/CeeblueTV/web-utils which is released under GNU Affero General Public License.
* See file LICENSE or go to https://spdx.org/licenses/AGPL-3.0-or-later.html for full license details.
*/
let _logging = 0;
setInterval(() => {
console.assert(_logging === 0, _logging.toFixed(), 'calls to log was useless');
}, 10000);
/**
* Log types
*/
var LogType;
(function (LogType) {
LogType["ERROR"] = "error";
LogType["WARN"] = "warn";
LogType["INFO"] = "info";
LogType["DEBUG"] = "debug";
})(LogType || (LogType = {}));
/**
* Log instance
*/
class Log {
get error() {
return this._bind(LogType.ERROR);
}
get warn() {
return this._bind(LogType.WARN);
}
get info() {
return this._bind(LogType.INFO);
}
get debug() {
return this._bind(LogType.DEBUG);
}
constructor(onLog, ...args) {
if (!args.length) {
// cannot have 0 args to be called correctly!
args.push(undefined);
}
this._args = args;
this._onLog = onLog;
++_logging;
}
_bind(type) {
if (!this._done) {
this._done = true;
--_logging;
}
// call the local onLog
if (this._onLog) {
this._onLog(type, this._args);
}
// call the global onLog
if (this._args.length && log.on) {
log.on(type, this._args);
}
// if not intercepted display the log
return this._args.length ? console[type].bind(console, ...this._args) : EMPTY_FUNCTION;
}
}
/**
* Inherits from this class to use logs
*/
class Loggable {
constructor() {
/**
* Start a log
* @param args
* @returns a Log object with the levels of log to call
*/
this.log = (...args) => {
return new Log(this.log.on, ...args);
};
}
}
/**
* Global log
*/
const log = (...args) => {
return new Log(() => {}, ...args);
}; /**
* Copyright 2024 Ceeblue B.V.
* This file is part of https://github.com/CeeblueTV/web-utils which is released under GNU Affero General Public License.
* See file LICENSE or go to https://spdx.org/licenses/AGPL-3.0-or-later.html for full license details.
*/
/**
* A advanced EventEmitter which allows to declare event as natural function in the inheriting children class,
* function must start by `on` prefix to be recognized as an event.
* The function can define a behavior by default, and user can choose to redefine this behavior,
* or add an additionnal subscription for this event.
* In addition you can unsubscribe to multiple events with an `AbortController`
* @example
* class Logger extends EventEmitter {
* onLog(log:string) { console.log(log); } // behavior by default
*
* test() {
* // raise event onLog
* this.onLog('test');
* }
* }
*
* const logger = new Logger();
* logger.test(); // displays a log 'test'
*
* // redefine default behavior to display nothing
* logger.onLog = () => {}
* logger.test(); // displays nothing
*
* // add an additionnal subscription
* logger.on('log', console.log);
* logger.test(); // displays a log 'test'
*
* // remove the additionnal subscription
* logger.off('log', console.log);
* logger.test(); // displays nothing
*
* // add two additionnal subscriptions with a AbortController
* const controller = new AbortController();
* logger.on('log', log => console.log(log), controller);
* logger.on('log', log => console.error(log), controller);
* logger.test(); // displays a log 'test' + an error 'test'
*
* // Unsubscribe all the subscription with the AbortController
* controller.abort();
* logger.test(); // displays nothing
*/
class EventEmitter$1 extends Loggable {
/**
* Build our EventEmitter, usually call from children class
*/
constructor() {
super();
this._events = new Map();
// Fill events with events as defined!
let proto = Object.getPrototypeOf(this);
while (proto && proto !== Object.prototype) {
for (const name of Object.getOwnPropertyNames(proto)) {
if (name.length < 3 || !name.startsWith('on')) {
continue;
}
if (proto[name] instanceof Function) {
const events = new Set();
this._events.set(name.substring(2).toLowerCase(), events);
let defaultEvent = proto[name];
const raise = (...args) => {
// Call default event if not null (can happen in JS usage)
if (defaultEvent) {
defaultEvent.call(this, ...args);
}
// Call subscribers
for (const event of events) {
event(...args);
}
};
Object.defineProperties(this, {
[name]: {
get: () => raise,
set: value => {
// Assign a default behavior!
defaultEvent = value;
}
}
});
}
}
proto = Object.getPrototypeOf(proto);
}
}
/**
* Event subscription
* @param name Name of event without the `on` prefix (ex: `log` to `onLog` event declared)
* @param event Subscriber Function
* @param abort Optional `AbortController` to stop this or multiple subscriptions in same time
*/
on(name, event, abort) {
if (!event) {
throw Error('event to subscribe cannot be null');
}
const events = this._event(name);
events.add(event);
if (abort) {
abort.signal.addEventListener('abort', () => events.delete(event), {
once: true
});
}
}
/**
* Event subscription only one time, once time fired it's automatically unsubscribe
* @param name Name of event without the `on` prefix (ex: `log` to `onLog` event declared)
* @param event Subscriber Function
* @param abort Optional `AbortController` to stop this or multiple subscriptions in same time
*/
once(name, event, abort) {
if (!event) {
throw Error('event to subscribe cannot be null');
}
const events = this._event(name);
events.add((...args) => {
events.delete(event); // delete from events
event(...args); // execute event
});
if (abort) {
abort.signal.addEventListener('abort', () => events.delete(event), {
once: true
});
}
}
/**
* Event unsubscription
* @param name Name of event without the 'on' prefix (ex: 'log' to 'onLog' event declared)
* @param event Unsubscriber Function, must be the one passed to {@link on} or {@link once} subscription methods
*/
off(name, event) {
if (!event) {
throw Error('event to unsubscribe cannot be null');
}
this._event(name).delete(event);
}
_event(name) {
const events = this._events.get(name.toLowerCase());
if (!events) {
throw Error('No event on' + name + ' on class ' + this.constructor.name);
}
return events;
}
} /**
* Copyright 2024 Ceeblue B.V.
* This file is part of https://github.com/CeeblueTV/web-utils which is released under GNU Affero General Public License.
* See file LICENSE or go to https://spdx.org/licenses/AGPL-3.0-or-later.html for full license details.
*/
/**
* Some fix for JS MAP:
* - find(key) search an item in the map and returns undefined if not found
* - get(key) return the item if exists or otherwise create and returns it
* - set(key, value) returns the value of the item (rather the MAP)
*/
class FixMap {
[Symbol.iterator]() {
return this._map[Symbol.iterator]();
}
get size() {
return this._map.size;
}
constructor(_initValue) {
this._initValue = _initValue;
this._map = new Map();
}
get(key) {
let value = this.find(key);
if (value === undefined) {
this._map.set(key, value = this._initValue());
}
return value;
}
find(key) {
return this._map.get(key);
}
has(key) {
return this._map.has(key);
}
clear() {
this._map.clear();
}
delete(key) {
return this._map.delete(key);
}
set(key, value) {
this._map.set(key, value);
return value;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
forEach(callbackfn, thisArg) {
this._map.forEach(callbackfn, thisArg);
}
} /**
* Copyright 2024 Ceeblue B.V.
* This file is part of https://github.com/CeeblueTV/web-utils which is released under GNU Affero General Public License.
* See file LICENSE or go to https://spdx.org/licenses/AGPL-3.0-or-later.html for full license details.
*/
/**
* Queue typed similar to a {@link https://en.cppreference.com/w/cpp/container/queue | std::queue<Type>} with possibility to limit the capacity like a FIFO
* @example
* const queue = new Queue<number>(2);
* queue.push(1); // [1]
* queue.push(2); // [1,2]
* queue.push(3); // [2,3] 1 has been removed to respect 2 capacity
*/
class Queue {
/**
* Number of element in the queue
*/
get size() {
return this._queue.length;
}
/**
* Maximum capacity for the queue, if not set queue has unlimited capacity
*/
get capacity() {
return this._capacity;
}
/**
* Set a maximum capacity for the queue,
* if you push new value exceding this capacity the firsts are removed (FIFO)
* if set to undefined the queue is unlimited
*/
set capacity(value) {
this._capacity = value;
if (value != null && this._queue.length > value) {
this._queue.splice(0, this._queue.length - value);
}
}
/**
* The front element
*/
get front() {
return this._queue[0];
}
/**
* The back element
*/
get back() {
return this._queue[this._queue.length - 1];
}
/**
* Iterator though queue's elements
*/
[Symbol.iterator]() {
return this._queue[Symbol.iterator]();
}
/**
* Instanciate a new queue object with the type passed as template
* @param capacity if set it limits the size of the queue, any exceding element pops the first element pushed (FIFO)
*/
constructor(capacity) {
this._capacity = capacity;
this._queue = new Array();
}
/**
* Push a new element in the queue
* @param value value of the element
* @returns this
*/
push(value) {
if (this._capacity != null && this._queue.push(value) > this._capacity) {
this.pop();
}
return this;
}
/**
* Pop the first element from the queue
* @returns The first element removed
*/
pop() {
return this._queue.shift();
}
/**
* Clear all the elements, queue becomes empty
* @returns this
*/
clear() {
this._queue.length = 0;
return this;
}
} /**
* Copyright 2024 Ceeblue B.V.
* This file is part of https://github.com/CeeblueTV/web-utils which is released under GNU Affero General Public License.
* See file LICENSE or go to https://spdx.org/licenses/AGPL-3.0-or-later.html for full license details.
*/
/**
* A collection of number Queue<number> with the following efficient mathematic computation:
* - minimum value in the collection
* - maximum value in the collection
* - average value of the collection
*/
class Numbers extends Queue {
/**
* minimum value in the collection, or 0 if colleciton is empty
*/
get minimum() {
return this._min;
}
/**
* maximum value in the collection, or 0 if colleciton is empty
*/
get maximum() {
return this._max;
}
/**
* average value of the collection, or 0 if collection if empty
*/
get average() {
if (this._average == null) {
this._average = this.size ? this._sum / this.size : 0;
}
return this._average;
}
/**
* Instantiate the collection of the number
* @param capacity if set it limits the number of values stored, any exceding number pops the first number pushed (FIFO)
*/
constructor(capacity) {
super(capacity);
this._sum = 0;
this._min = 0;
this._max = 0;
}
/**
* Push a value to the back to the collection
* @param value number to add
* @returns this
*/
push(value) {
if (value > this._max) {
this._max = value;
} else if (value < this._min) {
this._min = value;
}
this._average = undefined;
this._sum += value;
super.push(value);
return this;
}
/**
* Pop the front number from the collection
* @returns the front number removed
*/
pop() {
const front = super.pop();
if (front === this._max) {
this._max = Math.max(0, ...this);
} else if (front === this._min) {
this._min = Math.min(0, ...this);
}
this._average = undefined;
this._sum -= front || 0;
return front;
}
/**
* Clear all the numbers, collection becomes empty
* @returns this
*/
clear() {
this._min = this._max = this._sum = 0;
super.clear();
return this;
}
} /**
* Copyright 2024 Ceeblue B.V.
* This file is part of https://github.com/CeeblueTV/web-utils which is released under GNU Affero General Public License.
* See file LICENSE or go to https://spdx.org/licenses/AGPL-3.0-or-later.html for full license details.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* Toolkit to deal with Session Description Protocol (SDP),
* mainly to tranform a SDP string Offer/Answer to a manipulable JS object representation
* @example
* const peerConnection = new RTCPeerConnection();
* peerConnection.createOffer()
* .then( offer =>
* // Change offer.sdp string to a JS manipulable object
* const sdp = SDP.fromString(offer.sdp || '');
* // Change a property of SDP
* sdp.v = 2; // change SDP version
* // Reserialize to the legal SDP string format
* offer.sdp = SDP.toString(sdp);
* // Set SDP offer to peerConnection
* peerConnection.setLocalDescription(offer);
* )
*/
const SDP = {
/**
* Unserialize SDP string to a manipulable JS object representation
* It's an object with:
* - root SDP properties
* - media description iterable
* @param lines SDP string reprensentation
* @returns SDP object representation, iterable through media description
* @example
* [
* group: "DUNBLE 0 1",
* o: "- 1699450751193623 0 IN IP4 0.0.0.0",
* s: "-",
* t: "0 0",
* v: "0",
* ice-lite: "",
* length: 2,
* {
* m: "audio 9 UDP/TLS/RTP/SAVPF 111",
* c: "IN IP4 0.0.0.0",
* rtcp: "9",
* sendonly: "",
* setup: "passive",
* fingerprint: "sha-256 51:36:ED:78:A4:9F:25:8C:39:9A:0E:A0:B4:9B:6E:04:37:FF:AD:96:93:71:43:88:2C:0B:0F:AB:6F:9A:52:B8",
* ice-ufrag: "fa37",
* ice-pwd: "JncCHryDsbzayy4cBWDxS2",
* rtcp-mux: "",
* rtcp-rsize: "",
* rtpmap: "111 opus/48000/2",
* rtcp-fb: "111 nack",
* id: "0",
* fmtp: "111 minptime=10;useinbandfec=1",
* candidate: "1 1 udp 2130706431 89.105.221.108 56643 typ host",
* end-of-candidates: ""
* },
* {
* m: "video 9 UDP/TLS/RTP/SAVPF 106",
* c: "IN IP4 0.0.0.0",
* rtcp: "9",
* sendonly: "",
* setup: "passive",
* fingerprint: "sha-256 51:36:ED:78:A4:9F:25:8C:39:9A:0E:A0:B4:9B:6E:04:37:FF:AD:96:93:71:43:88:2C:0B:0F:AB:6F:9A:52:B8",
* ice-ufrag: "fa37",
* ice-pwd: "JncCHryDsbzayy4cBWDxS2",
* rtcp-mux: "",
* rtcp-rsize: "",
* rtpmap: "106 H264/90000",
* rtcp-fb: [
* "106 nack",
* "106 goog-remb"
* ],
* mid: "1",
* fmtp: "106 profile-level-id=42e01f;level-asymmetry-allowed=1;packetization-mode=1",
* candidate: "1 1 udp 2130706431 89.105.221.108 56643 typ host",
* end-of-candidates: ""
* }
* ]
*/
fromString(lines) {
if (Array.isArray(lines)) {
return lines; // already converted
}
const sdp = new Array();
let media = sdp;
let fingerprint;
for (let line of lines.toString().split('\n')) {
line = line.trim();
if (!line) {
continue;
}
let key = line[0];
const value = line.substring(line.indexOf('=') + 1).trim();
switch (key.toLowerCase()) {
case 'a':
{
if (!value) {
continue;
} // empty attribute!
key = this.addAttribute(media, value);
// save fingerprint to repeat it in medias
if (sdp === media && key.toLowerCase() === 'fingerprint') {
fingerprint = media.fingerprint;
}
break;
}
case 'm':
if (sdp.length && fingerprint && !sdp[sdp.length - 1].fingerprint) {
media.fingerprint = fingerprint;
}
sdp.push(media = {
m: value
});
break;
default:
media[key] = value;
}
}
if (sdp.length && fingerprint && !sdp[sdp.length - 1].fingerprint) {
media.fingerprint = fingerprint;
}
return sdp;
},
/**
* Serialize SDP JS object to a SDP string legal representation
* @param sdp SDP object reprensetation
* @returns SDP string reprensentation
*/