@ceeblue/webrtc-client
Version:
Ceeblue WebRTC Client
1,430 lines (1,420 loc) • 593 kB
JavaScript
/******************************************************************************
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() {
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 (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 = () => { };
/**
* Returns an efficient timestamp in milliseconds elapsed since performance.timeOrigin,
* representing the start of the current JavaScript execution context.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin
*
* @remarks Each Web Worker runs in a separate JS context, so timestamps
* are not directly comparable between different workers. Use {@link unixTime}
* for comparable timestamps across different Web Workers.
*/
function time() {
return Math.floor(_perf.now());
}
/**
* Returns an efficient Unix timestamp in milliseconds.
*
* Unix timestamps are universally comparable across different JavaScript
* contexts (e.g., Web Workers), as they reference the number of milliseconds
* elapsed since January 1, 1970 (UNIX epoch).
*/
function unixTime() {
return Math.floor(_perf.timeOrigin + _perf.now());
}
/**
* 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 (_a) {
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 = Object.assign({ withType: false, noEmptyString: false }, params);
const obj = {};
if (!value) {
return obj;
}
for (const [key, val] of iterableEntries(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 a IterableIterator<[string, any]> from any iterable input like Map, Set, Array, or Object.
*
* For all other types of values (including `null` or `undefined`) it returns an empty iterator.
*
* @param value An iterable input
* @returns a IterableIterator<[string, any]>
*/
function iterableEntries(value) {
if (!value) {
return (function* () { })();
}
if (typeof value.entries === 'function') {
value = value.entries();
}
if (typeof value[Symbol.iterator] === 'function') {
return value;
}
return (function* () {
for (const key in value) {
yield [key.toString(), 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 `2`, 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 = Object.assign({ space: ' ', decimal: 2, recursion: 2, 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 typeof obj === 'object' ? Object.prototype.toString.call(obj) : String(obj);
}
const space = params.space;
if (obj instanceof Set) {
let res = 'Set[';
let first = true;
for (const value of obj.values()) {
if (!first) {
res += ',' + space;
}
first = false;
res += stringify(value, Object.assign(Object.assign({}, params), { recursion: params.recursion - 1 }));
}
return res + ']';
}
if (obj instanceof Map) {
let res = 'Map{';
let first = true;
for (const [k, v] of obj.entries()) {
if (!first) {
res += ',' + space;
}
first = false;
res += stringify(k, Object.assign(Object.assign({}, params), { recursion: params.recursion - 1 }));
res += ':' + stringify(v, Object.assign(Object.assign({}, params), { recursion: params.recursion - 1 }));
}
return res + '}';
}
if (Array.isArray(obj)) {
// Array!
let res = '[';
let first = true;
for (const value of obj) {
if (!first) {
res += ',' + space;
}
first = false;
res += stringify(value, Object.assign(Object.assign({}, params), { recursion: params.recursion - 1 }));
}
return res + ']';
}
let res = '{';
let first = true;
for (const name in obj) {
if (!first) {
res += ',' + space;
}
first = false;
res += name + ':';
res += stringify(obj[name], Object.assign(Object.assign({}, params), { recursion: params.recursion - 1 }));
}
return res + '}';
}
/**
* 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 __awaiter(this, void 0, void 0, function* () {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
});
}
/**
* Test equality between two value whatever their type, object and array included
*/
function equal(a, b, seen = new WeakMap()) {
// 1. Check primitve identiy or NaN
if (a === b) {
return true;
}
if (Number.isNaN(a) && Number.isNaN(b)) {
return true;
}
// 2. Check the both are complexe object
if (Object(a) !== a || Object(b) !== b) {
return false;
}
// 3. Catch circular reference
if (seen.has(a)) {
return seen.get(a) === b;
}
seen.set(a, b);
// 4. Check « toStringTag » (Date, RegExp, Map, Set…)
const tagA = Object.prototype.toString.call(a);
if (tagA !== Object.prototype.toString.call(b)) {
return false;
}
// 5. Special Case
switch (tagA) {
case '[object Date]':
return a.getTime() === b.getTime();
case '[object RegExp]':
return a.source === b.source && a.flags === b.flags;
case '[object Set]':
case '[object Map]': {
if (a.size !== b.size) {
return false;
}
const aKeys = a.keys();
const bKeys = a.keys();
const aValues = a.values();
const bValues = b.values();
let aKey;
while (!(aKey = aKeys.next()).done) {
if (!equal(aKey.value, bKeys.next().value)) {
return false;
}
const aValue = aValues.next().value;
const bValue = bValues.next().value;
if (aValue !== aKey && !equal(aValue, bValue)) {
return false;
}
}
return true;
}
}
// 6. Arrays
if (Array.isArray(a)) {
if (!Array.isArray(b) || a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (!equal(a[i], b[i], seen)) {
return false;
}
}
return true;
}
// 7. Generic object : keys + symbols
const keysA = [...Object.keys(a), ...Object.getOwnPropertySymbols(a)];
if (keysA.length !== Object.keys(b).length + Object.getOwnPropertySymbols(b).length) {
return false;
}
for (const key of keysA) {
if (!equal(a[key], b[key], seen)) {
return false;
}
}
return true;
}
/**
* Fetch help method adding an explicit error property when Response is NOK, with the more accurate textual error inside
*/
function fetch$1(input, init) {
return __awaiter(this, void 0, void 0, function* () {
const response = (yield self.fetch(input, init));
if (!response.ok) {
if (response.body) {
response.error = yield response.text();
}
if (!response.error) {
response.error = response.statusText || response.status.toString() || 'Unknown error';
}
}
return response;
});
}
/**
* Fetch help method adding an explicit error property when Response is NOK, with the more accurate textual error inside
* Also measure the rtt of fetching and returns it in the property Response.rtt (guaranteed to be ≥ 1),
* supports subtracting server processing time using either the Response-Delay or CMSD-rd header when available
*
* WIP => replace the current implementation to use Resource Timing API
*/
function fetchWithRTT(input, init) {
return __awaiter(this, void 0, void 0, function* () {
// A first OPTIONS request to establish a connection (keep-alive)
yield fetch$1(input, Object.assign(Object.assign({}, init), { method: 'OPTIONS' }));
// Actual RTT measurement
const startTime = time();
const response = (yield fetch$1(input, init));
response.rtt = time() - startTime;
// remove the ResponseDelay if indicated by the server
let responseDelay = Number(response.headers.get('Response-Delay')) || 0;
if (!responseDelay) {
// search if we have a CMSD info?
// cmsd-dynamic "fly";rd=1
const cmsd = response.headers.get('cmsd-dynamic');
if (cmsd) {
for (const param of cmsd.split(';')) {
const [name, value] = param.split('=');
if (name.trim().toLowerCase() === 'rd') {
responseDelay = Number(value) || responseDelay;
}
}
}
}
response.rtt = Math.max(1, response.rtt - responseDelay);
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);
}
/**
* Wraps an object with a Proxy that makes property access case-insensitive.
*
* Property lookup (e.g. `obj.Foo` or `obj.foo`) will resolve to the same underlying key, regardless of casing.
* Only affects string-based property access (not symbols).
*
* @param obj - The original object.
* @returns A proxied object with case-insensitive property access.
*/
function caseInsensitive(obj) {
return new Proxy(obj, {
get(target, prop, receiver) {
if (typeof prop === 'string') {
const key = Object.keys(target).find(k => k.toLowerCase() === prop.toLowerCase());
if (key !== undefined) {
return Reflect.get(target, key, receiver);
}
}
return Reflect.get(target, prop, receiver);
}
});
}var Util=/*#__PURE__*/Object.freeze({__proto__:null,EMPTY_FUNCTION:EMPTY_FUNCTION,caseInsensitive:caseInsensitive,equal:equal,fetch:fetch$1,fetchWithRTT:fetchWithRTT,getBaseFile:getBaseFile,getExtension:getExtension,getFile:getFile,iterableEntries:iterableEntries,objectFrom:objectFrom,options:options,safePromise:safePromise,sleep:sleep,stringify:stringify,time:time,toBin:toBin,trim:trim,trimEnd:trimEnd,trimStart:trimStart,unixTime:unixTime});/**
* 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.
*/
/**
* 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) {
var _a;
let bin;
if (typeof data === 'string') {
// Convertit la chaîne en Uint8Array
bin = toBin(data);
}
else if (data instanceof ArrayBuffer) {
bin = new Uint8Array(data);
}
else if ('buffer' in data) {
bin = new Uint8Array(data.buffer, (_a = data.byteOffset) !== null && _a !== void 0 ? _a : 0, data.byteLength);
}
else {
bin = data;
}
this.reserve(this._size + bin.length);
this._data.set(bin, this._size);
this._size += bin.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(toBin(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.
*/
/**
* Log levels
*/
var LogLevel;
(function (LogLevel) {
LogLevel["ERROR"] = "error";
LogLevel["WARN"] = "warn";
LogLevel["INFO"] = "info";
LogLevel["DEBUG"] = "debug";
})(LogLevel || (LogLevel = {}));
// check coder issuer: everytime we don't forget to use the built Log
let _logging = 0;
setInterval(() => {
console.assert(_logging === 0, _logging.toFixed(), 'calls to log was useless');
}, 10000);
// !cb-override-log-level
const _overrideLogLevel = options()['!cb-override-log-level'];
const _charLevels = new Array(128);
_charLevels[101] = _charLevels[69] = 1; // error
_charLevels[119] = _charLevels[87] = 2; // warn
_charLevels[105] = _charLevels[73] = 3; // info
_charLevels[100] = _charLevels[68] = 4; // debug
/**
* Log instance
*/
class Log {
get error() {
return this._bind(LogLevel.ERROR);
}
get warn() {
return this._bind(LogLevel.WARN);
}
get info() {
return this._bind(LogLevel.INFO);
}
get debug() {
return this._bind(LogLevel.DEBUG);
}
constructor(log, ...args) {
if (!args.length) {
// cannot have 0 args to be called correctly!
args.push(undefined);
}
this._args = args;
this._log = log;
++_logging;
}
_onLog(localLog, level) {
var _a, _b;
// we take like log-level by priority order:
// 1- the overrideLogLevel
// 2- the localLog.level
// 3- the global log.level
// 4- LogLevel.INFO
const logLevel = (_b = (_a = _overrideLogLevel !== null && _overrideLogLevel !== void 0 ? _overrideLogLevel : localLog.level) !== null && _a !== void 0 ? _a : log.level) !== null && _b !== void 0 ? _b : LogLevel.INFO;
if (logLevel === false) {
// explicit null, no log at all!
return false;
}
if (logLevel !== true && _charLevels[level.charCodeAt(0)] > _charLevels[logLevel.charCodeAt(0)]) {
return false;
}
if (localLog.on) {
localLog.on(level, this._args);
}
return this._args.length ? true : false;
}
_bind(level) {
if (!this._done) {
this._done = true;
--_logging;
}
// call the global onLog in first (global filter)
if (!this._onLog(log, level)) {
return EMPTY_FUNCTION;
}
// call the local onLog (local filter)
if (this._log !== log && !this._onLog(this._log, level)) {
return EMPTY_FUNCTION;
}
// if not intercepted display the log
return console[level].bind(console, ...this._args);
}
}
/**
* 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, ...args);
});
}
}
/**
* Global log
*/
const log = ((...args) => {
return new Log(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.
*/
/**
* BitReader allows to read binary data bit by bit
*/
class BitReader extends Loggable {
constructor(data) {
super();
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) {
this.log('Exponential-Golomb code exceeding unsigned 16 bits').warn();
return 0;
}
return result + (1 << i) - 1;
}
}/**
* 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.
*/
/**
* Class to compute a weighted average byte rate over a specified time interval.
*
* This class continuously tracks data transmission and computes the byte rate
* based on a weighted average, considering both the duration and the number of
* bytes in each sample. It allows for real-time monitoring of bandwidth usage
* and provides mechanisms to dynamically adjust the measurement interval.
*
* Features:
* - Computes the byte rate using a **weighted average** approach.
* - Allows setting a custom interval for tracking.
* - Supports dynamic clipping to manually shrink the observation window.
*/
class ByteRate {
/**
* Raised when new bytes are added
*/
onBytes(bytes) { }
/**
* Returns the interval used for computing the byte rate
*/
get interval() {
return this._interval;
}
/**
* Sets a new interval for computing the average byte rate
*/
set interval(value) {
this._interval = value;
this.updateSamples();
}
/**
* Constructor initializes the ByteRate object with a specified interval (default: 1000ms).
* It sets up necessary variables to track byte rate over time.
*
* @param interval - Time interval in milliseconds to compute the byte rate.
*/
constructor(interval = 1000) {
this._interval = interval;
this.clear();
}
/**
* Returns the computed byte rate rounded to the nearest integer
*/
value() {
return Math.round(this.exact());
}
/**
* Computes the exact byte rate in bytes per second
*/
exact() {
// compute rate/s
this.updateSamples();
const duration = time() - this._time;
return duration ? (this._bytes / duration) * 1000 : 0;
}
/**
* Adds a new byte sample to the tracking system.
* Updates the list of samples and recomputes the byte rate
*
* @param bytes - Number of bytes added in this interval
*/
addBytes(bytes) {
var _a;
const time$1 = time();
const lastSample = this.updateSamples(time$1)[this._samples.length - 1];
const lastTime = (_a = lastSample === null || lastSample === void 0 ? void 0 : lastSample.time) !== null && _a !== void 0 ? _a : this._time;
if (time$1 > lastTime) {
this._samples.push({ bytes, time: time$1, clip: false });
}
else {
// no new duration => attach byte to last-one
if (!lastSample) {
// Ignore, was before our ByteRate scope !
return this;
}
lastSample.bytes += bytes;
}
this._bytes += bytes;
this.onBytes(bytes);
return this;
}
/**
* Clears all recorded byte rate data.
*/
clear() {
this._bytes = 0;
this._time = time();
this._samples = [];
this._clip = false;
return this;
}
/**
* Clips the byte rate tracking by marking the last sample as clipped.
* If a previous clip exists, removes the clipped sample and all preceding samples.
* Allows to shrink the interval manually between two positions.
*/
clip() {
if (this._clip) {
this._clip = false;
let removes = 0;
for (const sample of this._samples) {
this._bytes -= sample.bytes;
++removes;
this._time = sample.time;
if (sample.clip) {
break;
}
}
this._samples.splice(0, removes);
}
const lastSample = this._samples[this._samples.length - 1];
if (lastSample) {
lastSample.clip = true;
this._clip = true;
}
return this;
}
updateSamples(now = time()) {
// Remove obsolete sample
const timeOK = now - this._interval;
let removes = 0;
let sample;
while (this._time < timeOK && (sample = this._samples[removes])) {
this._bytes -= sample.bytes;
if (sample.clip) {
this._clip = sample.clip = false;
}
if (sample.time > timeOK) {
// only a part of the sample to delete !
sample.bytes *= (sample.time - timeOK) / (sample.time - this._time);
this._time = timeOK;
this._bytes += sample.bytes;
break;
}
++removes;
this._time = sample.time;
}
this._samples.splice(0, removes);
return this._samples;
}
}/**
* 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 {
/**
* 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) {
// Remove leading slashes for absolute pathes!
address = address.replace(/^[\/]+/, '');
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 host part from address `(http://)domain:port/path)`
*/
get host() {
return this._host;
}
/**
* 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;
}
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 #/
}
// Remove Path!
pos = address.indexOf('/');
if (pos >= 0) {
address = address.substring(0, pos);
}
this._host = address;
this._domain = address;
this._port = defaultPort;
const domainPortMatch = this._host.match(/^(?:\[([0-9a-fA-F:]+)\]|([^:/?#]+))(?::(\d+))?(?=[/#?]|$)/);
if (domainPortMatch) {
this._domain = domainPortMatch[1] || domainPortMatch[2];
if (domainPortMatch[3]) {
const port = parseInt(domainPortMatch[3]);
if (port >= 0 && port <= 0xffff) {
this._port = port;
}
}
}
}
}/**
* 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["DIRECT_STREAMING"] = "DirectStreaming";
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) {
var _a;
// Compute appropriate mediaExt out parameter
if (!params.mediaExt) {
try {
const url = new URL(params.endPoint);
// Set mediaExt with ?ext= param when set OR url extension
params.mediaExt = (_a = url.searchParams.get('ext')) !== null && _a !== void 0 ? _a : getExtension(getFile(url.pathname));
}
catch (_b) {
// not an URL, it's only a host
params.mediaExt = '';
}
}
// Normalize mediaExt in removing the possible '.' prefix and change it to lower case
params.mediaExt = trimStart(params.mediaExt, '.').toLowerCase();
switch (type) {
case Type.DIRECT_STREAMING:
if (!params.mediaExt) {
params.mediaExt = 'mp4';
}
break;
case Type.HESP:
params.mediaExt = 'mp4';
break;
case Type.WEBRTC:
params.mediaExt = 'rtp';
break;
case Type.WRTS: {
// json means a manifest file endPoint, replace with default rts media extension
if (!params.mediaExt || params.mediaExt === 'json') {
params.mediaExt = 'rts';
}
break;
}
case Type.META:
params.mediaExt = 'js';
break;
case Type.DATA:
params.mediaExt = 'json';
break;
default:
log('Unknown params type ' + type).warn();
break;
}
}
/**
* Build an URL from {@link Type | type} and {@link Params | params}
* Can assign {@link Params.mediaExt | params.mediaExt} or {@link Params.streamName | params.streamName}
* @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') {
var _a;
defineMediaExt(type, params);
const url = new URL(NetAddress.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.DIRECT_STREAMING:
url.pathname = '/live/' + 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:
log('Unknown url type ' + type).warn();
break;
}
}
else {
// Host has already a path! keep it unchanged, it's user intentionnal (used with some other WHIP/WHEP server?)
if (!params.streamName) {
// extract the second part of the URL's path (the first part being the protocol name), or the f