Commit updated Javascript packages

This commit is contained in:
Owncast
2020-10-20 05:04:18 +00:00
parent 5d03ecb694
commit a88734348a
6368 changed files with 603959 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
export const isTypedArray = (obj) => ArrayBuffer.isView(obj);
export const toUint8 = (bytes) => (bytes instanceof Uint8Array) ?
bytes :
new Uint8Array(
bytes && bytes.buffer || bytes,
bytes && bytes.byteOffset || 0,
bytes && bytes.byteLength || 0
);
export const bytesToString = (bytes) => {
if (!bytes) {
return '';
}
bytes = Array.prototype.slice.call(bytes);
const string = String.fromCharCode.apply(null, toUint8(bytes));
try {
return decodeURIComponent(escape(string));
} catch (e) {
// if decodeURIComponent/escape fails, we are dealing with partial
// or full non string data. Just return the potentially garbled string.
}
return string;
};
export const stringToBytes = (string, stringIsBytes = false) => {
const bytes = [];
if (typeof string !== 'string' && string && typeof string.toString === 'function') {
string = string.toString();
}
if (typeof string !== 'string') {
return bytes;
}
// If the string already is bytes, we don't have to do this
if (!stringIsBytes) {
string = unescape(encodeURIComponent(string));
}
return string.split('').map((s) => s.charCodeAt(0) & 0xFF);
};
export const concatTypedArrays = (...buffers) => {
const totalLength = buffers.reduce((total, buf) => {
const len = buf && (buf.byteLength || buf.length);
total += len || 0;
return total;
}, 0);
const tempBuffer = new Uint8Array(totalLength);
let offset = 0;
buffers.forEach(function(buf) {
buf = toUint8(buf);
tempBuffer.set(buf, offset);
offset += buf.byteLength;
});
return tempBuffer;
};

View File

@@ -0,0 +1,198 @@
import window from 'global/window';
const regexs = {
// to determine mime types
mp4: /^(av0?1|avc0?[1234]|vp0?9|flac|opus|mp3|mp4a|mp4v)/,
webm: /^(vp0?[89]|av0?1|opus|vorbis)/,
ogg: /^(vp0?[89]|theora|flac|opus|vorbis)/,
// to determine if a codec is audio or video
video: /^(av0?1|avc0?[1234]|vp0?[89]|hvc1|hev1|theora|mp4v)/,
audio: /^(mp4a|flac|vorbis|opus|ac-[34]|ec-3|alac|mp3)/,
// mux.js support regex
muxerVideo: /^(avc0?1)/,
muxerAudio: /^(mp4a)/
};
/**
* Replace the old apple-style `avc1.<dd>.<dd>` codec string with the standard
* `avc1.<hhhhhh>`
*
* @param {string} codec
* Codec string to translate
* @return {string}
* The translated codec string
*/
export const translateLegacyCodec = function(codec) {
if (!codec) {
return codec;
}
return codec.replace(/avc1\.(\d+)\.(\d+)/i, function(orig, profile, avcLevel) {
const profileHex = ('00' + Number(profile).toString(16)).slice(-2);
const avcLevelHex = ('00' + Number(avcLevel).toString(16)).slice(-2);
return 'avc1.' + profileHex + '00' + avcLevelHex;
});
};
/**
* Replace the old apple-style `avc1.<dd>.<dd>` codec strings with the standard
* `avc1.<hhhhhh>`
*
* @param {string[]} codecs
* An array of codec strings to translate
* @return {string[]}
* The translated array of codec strings
*/
export const translateLegacyCodecs = function(codecs) {
return codecs.map(translateLegacyCodec);
};
/**
* Replace codecs in the codec string with the old apple-style `avc1.<dd>.<dd>` to the
* standard `avc1.<hhhhhh>`.
*
* @param {string} codecString
* The codec string
* @return {string}
* The codec string with old apple-style codecs replaced
*
* @private
*/
export const mapLegacyAvcCodecs = function(codecString) {
return codecString.replace(/avc1\.(\d+)\.(\d+)/i, (match) => {
return translateLegacyCodecs([match])[0];
});
};
/**
* @typedef {Object} ParsedCodecInfo
* @property {number} codecCount
* Number of codecs parsed
* @property {string} [videoCodec]
* Parsed video codec (if found)
* @property {string} [videoObjectTypeIndicator]
* Video object type indicator (if found)
* @property {string|null} audioProfile
* Audio profile
*/
/**
* Parses a codec string to retrieve the number of codecs specified, the video codec and
* object type indicator, and the audio profile.
*
* @param {string} [codecString]
* The codec string to parse
* @return {ParsedCodecInfo}
* Parsed codec info
*/
export const parseCodecs = function(codecString = '') {
const codecs = codecString.split(',');
const result = {};
codecs.forEach(function(codec) {
codec = codec.trim();
['video', 'audio'].forEach(function(name) {
const match = regexs[name].exec(codec.toLowerCase());
if (!match || match.length <= 1) {
return;
}
// maintain codec case
const type = codec.substring(0, match[1].length);
const details = codec.replace(type, '');
result[name] = {type, details};
});
});
return result;
};
/**
* Returns a ParsedCodecInfo object for the default alternate audio playlist if there is
* a default alternate audio playlist for the provided audio group.
*
* @param {Object} master
* The master playlist
* @param {string} audioGroupId
* ID of the audio group for which to find the default codec info
* @return {ParsedCodecInfo}
* Parsed codec info
*/
export const codecsFromDefault = (master, audioGroupId) => {
if (!master.mediaGroups.AUDIO || !audioGroupId) {
return null;
}
const audioGroup = master.mediaGroups.AUDIO[audioGroupId];
if (!audioGroup) {
return null;
}
for (const name in audioGroup) {
const audioType = audioGroup[name];
if (audioType.default && audioType.playlists) {
// codec should be the same for all playlists within the audio type
return parseCodecs(audioType.playlists[0].attributes.CODECS);
}
}
return null;
};
export const isVideoCodec = (codec = '') => regexs.video.test(codec.trim().toLowerCase());
export const isAudioCodec = (codec = '') => regexs.audio.test(codec.trim().toLowerCase());
export const getMimeForCodec = (codecString) => {
if (!codecString || typeof codecString !== 'string') {
return;
}
const codecs = codecString
.toLowerCase()
.split(',')
.map((c) => translateLegacyCodec(c.trim()));
// default to video type
let type = 'video';
// only change to audio type if the only codec we have is
// audio
if (codecs.length === 1 && isAudioCodec(codecs[0])) {
type = 'audio';
}
// default the container to mp4
let container = 'mp4';
// every codec must be able to go into the container
// for that container to be the correct one
if (codecs.every((c) => regexs.mp4.test(c))) {
container = 'mp4';
} else if (codecs.every((c) => regexs.webm.test(c))) {
container = 'webm';
} else if (codecs.every((c) => regexs.ogg.test(c))) {
container = 'ogg';
}
return `${type}/${container};codecs="${codecString}"`;
};
export const browserSupportsCodec = (codecString = '') => window.MediaSource &&
window.MediaSource.isTypeSupported &&
window.MediaSource.isTypeSupported(getMimeForCodec(codecString)) || false;
export const muxerSupportsCodec = (codecString = '') => codecString.toLowerCase().split(',').every((codec) => {
codec = codec.trim();
return regexs.muxerVideo.test(codec) || regexs.muxerAudio.test(codec);
});
export const DEFAULT_AUDIO_CODEC = 'mp4a.40.2';
export const DEFAULT_VIDEO_CODEC = 'avc1.4d400d';

View File

@@ -0,0 +1,154 @@
import {bytesToString, toUint8} from './byte-helpers.js';
export const id3Size = function(bytes, offset = 0) {
bytes = toUint8(bytes);
const returnSize = (bytes[offset + 6] << 21) |
(bytes[offset + 7] << 14) |
(bytes[offset + 8] << 7) |
(bytes[offset + 9]);
const flags = bytes[offset + 5];
const footerPresent = (flags & 16) >> 4;
if (footerPresent) {
return returnSize + 20;
}
return returnSize + 10;
};
export const getId3Offset = function(bytes, offset = 0) {
bytes = toUint8(bytes);
if ((bytes.length - offset) < 10 || bytesToString(bytes.subarray(offset, offset + 3)) !== 'ID3') {
return offset;
}
offset += id3Size(bytes, offset);
// recursive check for id3 tags as some files
// have multiple ID3 tag sections even though
// they should not.
return getId3Offset(bytes, offset);
};
export const isLikely = {
aac(bytes) {
const offset = getId3Offset(bytes);
return bytes.length >= offset + 2 &&
(bytes[offset] & 0xFF) === 0xFF &&
(bytes[offset + 1] & 0xE0) === 0xE0 &&
(bytes[offset + 1] & 0x16) === 0x10;
},
mp3(bytes) {
const offset = getId3Offset(bytes);
return bytes.length >= offset + 2 &&
(bytes[offset] & 0xFF) === 0xFF &&
(bytes[offset + 1] & 0xE0) === 0xE0 &&
(bytes[offset + 1] & 0x06) === 0x02;
},
webm(bytes) {
return bytes.length >= 4 &&
(bytes[0] & 0xFF) === 0x1A &&
(bytes[1] & 0xFF) === 0x45 &&
(bytes[2] & 0xFF) === 0xDF &&
(bytes[3] & 0xFF) === 0xA3;
},
mp4(bytes) {
return bytes.length >= 8 &&
(/^(f|s)typ$/).test(bytesToString(bytes.subarray(4, 8))) &&
// not 3gp data
!(/^ftyp3g$/).test(bytesToString(bytes.subarray(4, 10)));
},
'3gp'(bytes) {
return bytes.length >= 10 &&
(/^ftyp3g$/).test(bytesToString(bytes.subarray(4, 10)));
},
ts(bytes) {
if (bytes.length < 189 && bytes.length >= 1) {
return bytes[0] === 0x47;
}
let i = 0;
// check the first 376 bytes for two matching sync bytes
while (i + 188 < bytes.length && i < 188) {
if (bytes[i] === 0x47 && bytes[i + 188] === 0x47) {
return true;
}
i += 1;
}
return false;
},
flac(bytes) {
return bytes.length >= 4 &&
(/^fLaC$/).test(bytesToString(bytes.subarray(0, 4)));
},
ogg(bytes) {
return bytes.length >= 4 &&
(/^OggS$/).test(bytesToString(bytes.subarray(0, 4)));
}
};
// get all the isLikely functions
// but make sure 'ts' is at the bottom
// as it is the least specific
const isLikelyTypes = Object.keys(isLikely)
// remove ts
.filter((t) => t !== 'ts')
// add it back to the bottom
.concat('ts');
// make sure we are dealing with uint8 data.
isLikelyTypes.forEach(function(type) {
const isLikelyFn = isLikely[type];
isLikely[type] = (bytes) => isLikelyFn(toUint8(bytes));
});
// A useful list of file signatures can be found here
// https://en.wikipedia.org/wiki/List_of_file_signatures
export const detectContainerForBytes = (bytes) => {
bytes = toUint8(bytes);
for (let i = 0; i < isLikelyTypes.length; i++) {
const type = isLikelyTypes[i];
if (isLikely[type](bytes)) {
return type;
}
}
return '';
};
// fmp4 is not a container
export const isLikelyFmp4MediaSegment = (bytes) => {
bytes = toUint8(bytes);
let i = 0;
while (i < bytes.length) {
const size = (bytes[i] << 24 | bytes[i + 1] << 16 | bytes[i + 2] << 8 | bytes[i + 3]) >>> 0;
const type = bytesToString(bytes.subarray(i + 4, i + 8));
if (type === 'moof') {
return true;
}
if (size === 0 || (size + i) > bytes.length) {
i = bytes.length;
} else {
i += size;
}
}
return false;
};

View File

@@ -0,0 +1,13 @@
import window from 'global/window';
const atob = (s) => window.atob ? window.atob(s) : Buffer.from(s, 'base64').toString('binary');
export default function decodeB64ToUint8Array(b64Text) {
const decodedString = atob(b64Text);
const array = new Uint8Array(decodedString.length);
for (let i = 0; i < decodedString.length; i++) {
array[i] = decodedString.charCodeAt(i);
}
return array;
}

View File

@@ -0,0 +1,22 @@
/**
* Loops through all supported media groups in master and calls the provided
* callback for each group
*
* @param {Object} master
* The parsed master manifest object
* @param {string[]} groups
* The media groups to call the callback for
* @param {Function} callback
* Callback to call for each media group
*/
export const forEachMediaGroup = (master, groups, callback) => {
groups.forEach((mediaType) => {
for (const groupKey in master.mediaGroups[mediaType]) {
for (const labelKey in master.mediaGroups[mediaType][groupKey]) {
const mediaProperties = master.mediaGroups[mediaType][groupKey][labelKey];
callback(mediaProperties, mediaType, groupKey, labelKey);
}
}
});
};

View File

@@ -0,0 +1,36 @@
const MPEGURL_REGEX = /^(audio|video|application)\/(x-|vnd\.apple\.)?mpegurl/i;
const DASH_REGEX = /^application\/dash\+xml/i;
/**
* Returns a string that describes the type of source based on a video source object's
* media type.
*
* @see {@link https://dev.w3.org/html5/pf-summary/video.html#dom-source-type|Source Type}
*
* @param {string} type
* Video source object media type
* @return {('hls'|'dash'|'vhs-json'|null)}
* VHS source type string
*/
export const simpleTypeFromSourceType = (type) => {
if (MPEGURL_REGEX.test(type)) {
return 'hls';
}
if (DASH_REGEX.test(type)) {
return 'dash';
}
// Denotes the special case of a manifest object passed to http-streaming instead of a
// source URL.
//
// See https://en.wikipedia.org/wiki/Media_type for details on specifying media types.
//
// In this case, vnd stands for vendor, video.js for the organization, VHS for this
// project, and the +json suffix identifies the structure of the media type.
if (type === 'application/vnd.videojs.vhs+json') {
return 'vhs-json';
}
return null;
};

View File

@@ -0,0 +1,18 @@
import URLToolkit from 'url-toolkit';
import window from 'global/window';
const resolveUrl = (baseUrl, relativeUrl) => {
// return early if we don't need to resolve
if ((/^[a-z]+:/i).test(relativeUrl)) {
return relativeUrl;
}
// if the base URL is relative then combine with the current location
if (!(/\/\//i).test(baseUrl)) {
baseUrl = URLToolkit.buildAbsoluteURL(window.location && window.location.href || '', baseUrl);
}
return URLToolkit.buildAbsoluteURL(baseUrl, relativeUrl);
};
export default resolveUrl;

View File

@@ -0,0 +1,108 @@
/**
* @file stream.js
*/
/**
* A lightweight readable stream implemention that handles event dispatching.
*
* @class Stream
*/
export default class Stream {
constructor() {
this.listeners = {};
}
/**
* Add a listener for a specified event type.
*
* @param {string} type the event name
* @param {Function} listener the callback to be invoked when an event of
* the specified type occurs
*/
on(type, listener) {
if (!this.listeners[type]) {
this.listeners[type] = [];
}
this.listeners[type].push(listener);
}
/**
* Remove a listener for a specified event type.
*
* @param {string} type the event name
* @param {Function} listener a function previously registered for this
* type of event through `on`
* @return {boolean} if we could turn it off or not
*/
off(type, listener) {
if (!this.listeners[type]) {
return false;
}
const index = this.listeners[type].indexOf(listener);
// TODO: which is better?
// In Video.js we slice listener functions
// on trigger so that it does not mess up the order
// while we loop through.
//
// Here we slice on off so that the loop in trigger
// can continue using it's old reference to loop without
// messing up the order.
this.listeners[type] = this.listeners[type].slice(0);
this.listeners[type].splice(index, 1);
return index > -1;
}
/**
* Trigger an event of the specified type on this stream. Any additional
* arguments to this function are passed as parameters to event listeners.
*
* @param {string} type the event name
*/
trigger(type) {
const callbacks = this.listeners[type];
if (!callbacks) {
return;
}
// Slicing the arguments on every invocation of this method
// can add a significant amount of overhead. Avoid the
// intermediate object creation for the common case of a
// single callback argument
if (arguments.length === 2) {
const length = callbacks.length;
for (let i = 0; i < length; ++i) {
callbacks[i].call(this, arguments[1]);
}
} else {
const args = Array.prototype.slice.call(arguments, 1);
const length = callbacks.length;
for (let i = 0; i < length; ++i) {
callbacks[i].apply(this, args);
}
}
}
/**
* Destroys the stream and cleans up.
*/
dispose() {
this.listeners = {};
}
/**
* Forwards all `data` events on this stream to the destination stream. The
* destination stream should provide a method `push` to receive the data
* events as they arrive.
*
* @param {Stream} destination the stream that will receive all `data` events
* @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options
*/
pipe(destination) {
this.on('data', function(data) {
destination.push(data);
});
}
}